From 9bad3e98b54446d9746769a857613a3b94837e97 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 12 Sep 2025 22:17:48 +0800 Subject: [PATCH 001/193] =?UTF-8?q?=E8=AE=A1=E5=88=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=91=BD=E4=BB=A4=E8=A1=8C=EF=BC=8C=E4=BB=8E=E4=B8=A4?= =?UTF-8?q?=E6=AD=A5=E7=94=9F=E6=88=90=E5=8D=87=E7=BA=A7=E6=88=90=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 2 +- src/DotNetCampus.CommandLine/CommandLine.cs | 329 +-------------- src/DotNetCampus.CommandLine/CommandRunner.cs | 253 +++++------ .../CommandRunnerBuilderExtensions.cs | 171 ++++---- .../Compiler/CommandObjectCreator.cs | 7 +- .../Compiler/ICommandHandlerCollection.cs | 12 +- .../ICommandRunnerBuilder.cs | 36 ++ .../LegacyCommandLine.cs | 397 ++++++++++++++++++ .../LegacyCommandRunner.cs | 212 ++++++++++ .../LegacyCommandRunnerBuilderExtensions.cs | 200 +++++++++ .../Properties/Compatibility.cs | 27 ++ .../Utils/CommandLineConverter.cs | 2 +- .../DictionaryCommandHandlerCollection.cs | 8 +- ...neratedAssemblyCommandHandlerCollection.cs | 6 +- .../Utils/Handlers/TaskCommandHandler.cs | 80 ++++ 15 files changed, 1177 insertions(+), 565 deletions(-) create mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLine.cs create mode 100644 src/DotNetCampus.CommandLine/LegacyCommandRunner.cs create mode 100644 src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs create mode 100644 src/DotNetCampus.CommandLine/Properties/Compatibility.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index 06f80cc3..1f4678ac 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -184,7 +184,7 @@ private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray(builder, {{(model.GetKebabCaseCommandNames() is { } cn ? $"\"{cn}\"" : "null")}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance, handler); + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, {{(model.CommandNames is { } cn ? $"\"{cn}\"" : "\"\"")}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); } """; } diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index bbfe71ca..1fb4ada8 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -1,9 +1,5 @@ -using System.ComponentModel; using System.Diagnostics.Contracts; -using System.Globalization; -using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; -using DotNetCampus.Cli.Utils.Collections; namespace DotNetCampus.Cli; @@ -25,128 +21,18 @@ public class CommandLine : ICoreCommandRunnerBuilder /// /// 在特定的属性不指定时,默认应使用的大小写敏感性。 /// - public bool DefaultCaseSensitive { get; } - - /// - /// 获取命令行参数中猜测的多级命令名称。 - /// 请注意,此字符串中可能包含空格,表示多级命令名称。也可能比预期的更长,包含后续的一部分位置参数,因为暂时还无法确定那些位置参数是否是命令名称。 - /// - /// - /// - /// # 对于以下命令: - /// do something --option value - /// # 本属性的值为 "do something"。 - /// # 对于以下命令: - /// do something /var/file --option value - /// # 本属性的值为 "do something"(因为 /var/file 可以提前判断出来不可能是命令) - /// # 可能存在三种情况: - /// # 1. do 和 something 都是位置参数。 - /// # 2. do 是命令,something 是位置参数。 - /// # 3. do 和 something 都是命令。 - /// - /// 此属性保存这个 something 的值,待后续决定使用处理器时,根据处理器是否要求有命令来决定这个词是否是位置参数。
- /// 另外,**特别强调**,此属性的值可能是命名变体,例如命令行传入 DoSomething 时,此属性则是 Do-Something。 - ///
- internal string PossibleCommandNames { get; } - - /// - /// 如果此命令行是从 Web 请求的 URL 中解析出来的,则此属性保存 URL 的 Scheme 部分。 - /// - private string? MatchedUrlScheme { get; } - - /// - /// 适用于选项的多值处理方式。 - /// - private MultiValueHandling OptionMultiValueHandling { get; } - - /// - /// 适用于位置参数的多值处理方式。 - /// - private MultiValueHandling PositionalArgumentsMultiValueHandling { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写敏感。 - /// - private OptionDictionary LongOptionValuesDefault { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写敏感。 - /// - private OptionDictionary LongOptionValuesCaseSensitive { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写不敏感。 - /// - private OptionDictionary LongOptionValuesIgnoreCase { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写敏感。 - /// - private OptionDictionary ShortOptionValuesDefault { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写敏感。 - /// - private OptionDictionary ShortOptionValuesCaseSensitive { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写不敏感。 - /// - private OptionDictionary ShortOptionValuesIgnoreCase { get; } - - /// - /// 从命令行中解析出来的位置参数。 - /// - /// - /// 注意,位置参数的前几个值可能是命令名称;这取决于 和实际处理器的命令。 - /// - /// # 对于以下命令: - /// do something --option value - /// # 可能存在三种情况: - /// # 1. do 和 something 都是位置参数。 - /// # 2. do 是命令,something 是位置参数。 - /// # 3. do 和 something 都是命令。 - /// - /// 如果处理器决定将 something 作为命令名称,那么当需要取出位置参数时,此属性的第一个值需要排除。 - /// - private ReadOnlyListRange PositionalArguments { get; } + public bool DefaultCaseSensitive => ParsingOptions.CaseSensitive; private CommandLine() { - var options = OptionDictionary.Empty; - var arguments = new ReadOnlyListRange(); - CommandLineArguments = arguments; + CommandLineArguments = []; ParsingOptions = CommandLineParsingOptions.Flexible; - DefaultCaseSensitive = false; - PossibleCommandNames = ""; - MatchedUrlScheme = null; - OptionMultiValueHandling = MultiValueHandling.First; - PositionalArgumentsMultiValueHandling = MultiValueHandling.First; - LongOptionValuesCaseSensitive = options; - LongOptionValuesIgnoreCase = options; - LongOptionValuesDefault = options; - ShortOptionValuesCaseSensitive = options; - ShortOptionValuesIgnoreCase = options; - ShortOptionValuesDefault = options; - PositionalArguments = arguments; } private CommandLine(IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions = null) { CommandLineArguments = arguments; ParsingOptions = parsingOptions ?? CommandLineParsingOptions.Flexible; - DefaultCaseSensitive = parsingOptions?.CaseSensitive ?? false; - (MatchedUrlScheme, var result) = CommandLineConverter.ParseCommandLineArguments(arguments, parsingOptions); - PossibleCommandNames = result.PossibleCommandNames; - OptionMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.First : MultiValueHandling.Last; - PositionalArgumentsMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.SpaceAll : MultiValueHandling.SlashAll; - LongOptionValuesCaseSensitive = result.LongOptions.ToOptionLookup(true); - LongOptionValuesIgnoreCase = result.LongOptions.ToOptionLookup(false); - LongOptionValuesDefault = DefaultCaseSensitive ? LongOptionValuesCaseSensitive : LongOptionValuesIgnoreCase; - ShortOptionValuesCaseSensitive = result.ShortOptions.ToOptionLookup(true); - ShortOptionValuesIgnoreCase = result.ShortOptions.ToOptionLookup(false); - ShortOptionValuesDefault = DefaultCaseSensitive ? ShortOptionValuesCaseSensitive : ShortOptionValuesIgnoreCase; - PositionalArguments = result.Arguments; } /// @@ -180,218 +66,9 @@ public static CommandLine Parse(IReadOnlyList args, CommandLineParsingOp [Pure] public static CommandLine Parse(string singleLineCommandLineArgs, CommandLineParsingOptions? parsingOptions = null) { - var args = CommandLineConverter.SingleLineCommandLineArgsToArrayCommandLineArgs(singleLineCommandLineArgs); + var args = CommandLineConverter.SingleLineToList(singleLineCommandLineArgs); return new CommandLine(args, parsingOptions); } CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); - - /// - /// 尝试将命令行参数转换为指定类型的实例。 - /// - /// 要转换的类型。 - /// 转换后的实例。 - [Pure] - public T As() where T : class => CommandRunner.CreateInstance(this); - - /// - /// 尝试将命令行参数转换为指定类型的实例。 - /// - /// 由拦截器传入的命令处理器创建方法。 - /// 要转换的类型。 - /// 转换后的实例。 - [Pure, EditorBrowsable(EditorBrowsableState.Never)] - public T As(CommandObjectCreator creator) where T : class => CommandRunner.CreateInstance(this, creator); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortOption) => GetShortOption(shortOption.ToString(CultureInfo.InvariantCulture)); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetShortOption(string shortOption) - { - return ShortOptionValuesDefault.TryGetValue(shortOption, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(string optionName) - { - return LongOptionValuesDefault.TryGetValue(optionName, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 短名称选项。 - /// 选项的名称。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortName, string longName) => - // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 - GetOption(shortName) - // 其次使用长名称。 - ?? GetOption(longName); - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char optionName, bool caseSensitive) => - GetShortOption(optionName.ToString(CultureInfo.InvariantCulture), caseSensitive); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetShortOption(string shortOption, bool caseSensitive) - { - var optionValues = caseSensitive - ? ShortOptionValuesCaseSensitive - : ShortOptionValuesIgnoreCase; - return optionValues.TryGetValue(shortOption, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(string optionName, bool caseSensitive) - { - var optionValues = caseSensitive - ? LongOptionValuesCaseSensitive - : LongOptionValuesIgnoreCase; - return optionValues.TryGetValue(optionName, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 短名称选项。 - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortName, string longName, bool caseSensitive) => - // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 - GetOption(shortName, caseSensitive) - // 其次使用长名称。 - ?? GetOption(longName, caseSensitive); - - /// - /// 获取命令行参数中位置参数的值。 - /// - /// 获取指定索引处的参数值。 - /// 从索引处获取参数值的最长长度。当大于 1 时,会将这些值合并为一个字符串。 - /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 - /// 位置参数的值。 - [Pure] - public CommandLinePropertyValue? GetPositionalArgument(int index, int length, string? commandNames = null) - { - var commandLevel = GetCommandLevel(commandNames); - var positionalArgumentsStartIndex = Math.Max(0, commandLevel); - positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); - var realIndex = index + positionalArgumentsStartIndex; - return realIndex < 0 || realIndex >= PositionalArguments.Count - ? null - : new CommandLinePropertyValue( - PositionalArguments.Slice(realIndex, - Math.Min(length, PositionalArguments.Count - realIndex)), PositionalArgumentsMultiValueHandling); - } - - /// - /// 获取命令行参数中所有位置参数值的集合。 - /// - /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 - /// 命令行参数中位置参数值的集合。 - [Pure] - public IReadOnlyList GetPositionalArguments(string? commandNames = null) - { - var commandLevel = GetCommandLevel(commandNames); - var positionalArgumentsStartIndex = Math.Max(0, commandLevel); - positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); - return PositionalArguments.Slice(positionalArgumentsStartIndex, PositionalArguments.Count - 1); - } - - /// - /// 根据某个特定的命令名称字符串,获取此字符串中包含了多少级命令。 - /// - /// 命令名称字符串。 - /// 命令的层级数。 - private int GetCommandLevel(string? commandNames) - { - var possibleCommandNames = PossibleCommandNames; - - // 如果没有命令,则不需要排除任何位置参数。 - if (string.IsNullOrEmpty(commandNames) || string.IsNullOrEmpty(possibleCommandNames)) - { - return 0; - } -#if !NETCOREAPP3_1_OR_GREATER - if (commandNames is null) - { - return 0; - } -#endif - if (commandNames.Length > possibleCommandNames.Length) - { - return 0; - } - if (!possibleCommandNames.StartsWith(commandNames, DefaultCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) - { - return 0; - } - // 计算 possibleCommandNames 中有多少个空格。 - var commandLevel = 1; - for (var i = 0; i < commandNames.Length; i++) - { - if (possibleCommandNames[i] == ' ') - { - commandLevel++; - } - } - return commandLevel; - } - - /// - /// 输出传入的命令行参数字符串。 - /// - /// 传入的命令行参数字符串。 - [Pure] - public override string ToString() - { - return MatchedUrlScheme is { } scheme - ? $"{scheme}://{string.Join("/", PositionalArguments)}?{string.Join("&", LongOptionValuesCaseSensitive.Select(x => $"{x.Key}={string.Join("&", x.Value)}"))}" - : string.Join(" ", CommandLineArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); - } } diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index a447de68..0f6afda3 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -1,8 +1,6 @@ -using System.Collections.Concurrent; using System.ComponentModel; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Handlers; namespace DotNetCampus.Cli; @@ -11,202 +9,177 @@ namespace DotNetCampus.Cli; /// public class CommandRunner : ICommandRunnerBuilder, IAsyncCommandRunnerBuilder { - private static ConcurrentDictionary CommandObjectCreationInfos { get; } = new( -#if NET5_0_OR_GREATER - ReferenceEqualityComparer.Instance -#endif - ); - private readonly CommandLine _commandLine; - private readonly DictionaryCommandHandlerCollection _dictionaryCommandHandlers = new(); - private readonly ConcurrentDictionary _assemblyCommandHandlers = []; + + private readonly SortedList _creators = new(StringLengthDescendingComparer.CaseSensitive); internal CommandRunner(CommandLine commandLine) { _commandLine = commandLine; } - internal CommandRunner(CommandRunner commandRunner) - { - _commandLine = commandRunner._commandLine; - } + /// + public int Run() => RunAsync().Result; - /// - /// 供源生成器调用,注册一个专门用来处理主命令(Main Command)或子命令/多级子命令(Sub Command)的命令处理器。 - /// - /// 关联的命令。 - /// 命令处理器的创建方法。 - /// 选项类型,或命令处理器类型,或任意类型。 - [EditorBrowsable(EditorBrowsableState.Never)] - public static void Register(string? commandNames, CommandObjectCreator creator) - where T : class - { - CommandObjectCreationInfos[typeof(T)] = new CommandObjectCreationInfo(commandNames, creator); - } + /// + CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => this; - /// - /// 创建一个命令处理器实例。 - /// - /// 已解析的命令行参数。 - /// 命令处理器的类型。 - /// 命令处理器实例。 - internal static T CreateInstance(CommandLine commandLine) + /// + public Task RunAsync() { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + var (possibleCommandNames, creator) = MatchCreator(); + + if (creator is null) { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + throw new CommandNameNotFoundException( + string.IsNullOrEmpty(possibleCommandNames) + ? "No default command handler found. Please ensure that a default command handler is registered correctly." + : $"No command handler found for command '{possibleCommandNames}'. Please ensure that the command handler is registered correctly.", + possibleCommandNames); } - return (T)info.Creator(commandLine); + var handler = (ICommandHandler)creator(_commandLine); + return handler.RunAsync(); } - /// - /// 创建一个命令处理器实例。 - /// - /// 已解析的命令行参数。 - /// 命令处理器的创建方法。 - /// 命令处理器的类型。 - /// 命令处理器实例。 - internal static T CreateInstance(CommandLine commandLine, CommandObjectCreator creator) + private (string PossibleCommandNames, ExperimentalCommandObjectCreator? Creator) MatchCreator() { - return (T)creator(commandLine); - } + if (_creators.Count is 0) + { + return ("", null); + } - CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => this; + var maxLength = _creators.Keys[0].Length; + var header = _commandLine.GetHeader(maxLength); + var stringComparison = _commandLine.DefaultCaseSensitive + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; - /// - /// 添加一个命令处理器。 - /// - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal CommandRunner AddHandler() - where T : class, ICommandHandler - { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + foreach (var (command, info) in _creators) { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + if (header.StartsWith(command, stringComparison) + || info.CommandAliases.Any(alias => header.StartsWith(alias, stringComparison))) + { + return (header, info.Creator); + } } - _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => (T)info.Creator(cl)); - return this; + return (header, null); } /// /// 添加一个命令处理器。 /// - /// 由拦截器传入的的命令处理器的命令。 + /// 由拦截器传入的的命令处理器的命令, 表示此处理器没有命令名称。 /// 由拦截器传入的命令处理器创建方法。 - /// 命令处理器的类型。 + /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 /// 返回一个命令处理器构建器。 [EditorBrowsable(EditorBrowsableState.Never)] - internal CommandRunner AddHandler(string? command, CommandObjectCreator creator) - where T : class, ICommandHandler + internal CommandRunner AddHandlerCore(string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases + ) { - _dictionaryCommandHandlers.AddHandler(command, creator); - return this; - } - - /// - /// 添加一个命令处理器。 - /// - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal CommandRunner AddHandler(Func> handler) - where T : class - { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + var isAdded = _creators.TryAdd(command ?? "", new CommandObjectCreationInfo { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + Creator = creator, + CommandAliases = commandAliases ?? [], + }); + if (!isAdded) + { + throw new InvalidOperationException($"The command '{command}' is already registered."); } - - _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => new TaskCommandHandler( - () => (T)info.Creator(cl), - handler)); return this; } - /// - /// 添加一个命令处理器。 - /// - /// 由拦截器传入的的命令处理器的命令。 - /// 由拦截器传入的命令处理器创建方法。 - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal CommandRunner AddHandler(string? command, CommandObjectCreator creator, Func> handler) - where T : class + private readonly record struct CommandObjectCreationInfo { - _dictionaryCommandHandlers.AddHandler(command, cl => new TaskCommandHandler( - () => (T)creator(cl), - handler)); - return this; - } + public required ExperimentalCommandObjectCreator Creator { get; init; } - internal CommandRunner AddHandlers() - where T : ICommandHandlerCollection, new() - { - var c = new T(); - _assemblyCommandHandlers.TryAdd(c, c); - return this; + public required IReadOnlyList CommandAliases { get; init; } } +} - private ICommandHandler? MatchHandler() +file static class CommandRunnerExtensions +{ + /// + /// 获取命令行前几个字符组成的字符串(空格分隔),长度等于或轻微超过指定的最大长度,除非命令行本身没有那么长。 + /// + /// 要获取前缀的命令行。 + /// 要比较的长度。 + /// 命令行前几个字符组成的字符串(空格分隔)。 + public static string GetHeader(this CommandLine commandLine, int compareToLength) { - var possibleCommandNames = _commandLine.PossibleCommandNames; - - // 优先寻找单独添加的处理器。 - if (_dictionaryCommandHandlers.TryMatch(possibleCommandNames, _commandLine) is { } h1) + var args = commandLine.CommandLineArguments; + if (args.Count is 0 || compareToLength <= 0) { - return h1; + return ""; } - // 其次寻找程序集中自动搜集到的处理器。 - foreach (var handler in _assemblyCommandHandlers) + int index; + var currentLength = 0; + for (index = 0; index < args.Count; index++) { - if (handler.Value.TryMatch(possibleCommandNames, _commandLine) is { } h2) + if (index > 0) { - return h2; + // 加上空格的长度。 + currentLength++; } - } - // 如果没有找到,那么很可能此命令没有命令名称,需要使用默认的处理器。 - if (_dictionaryCommandHandlers.TryMatch("", _commandLine) is { } h3) - { - return h3; - } - foreach (var handler in _assemblyCommandHandlers) - { - if (handler.Value.TryMatch("", _commandLine) is { } h4) + var arg = args[index]; + var length = currentLength + arg.Length; + if (length > compareToLength) { - return h4; + break; } + + currentLength = length; } - // 如果连默认的处理器都没有找到,说明根本没有能处理此命令的处理器。 - return null; + return string.Join(" ", args.Take(index + 1)); } +} - /// - public int Run() - { - return RunAsync().Result; - } +/// +/// 按长度比较字符串的比较器。更长的字符串在排序中更靠前。 +/// +/// +file class StringLengthDescendingComparer(bool caseSensitive) : IComparer +{ + /// + /// 区分大小写的字符串长度降序比较器。 + /// + public static StringLengthDescendingComparer CaseSensitive { get; } = new StringLengthDescendingComparer(true); - /// - public Task RunAsync() + /// + /// 不区分大小写的字符串长度降序比较器。 + /// + public static StringLengthDescendingComparer CaseInsensitive { get; } = new StringLengthDescendingComparer(false); + + public int Compare(string? x, string? y) { - var handler = MatchHandler(); + if (x == null && y == null) + { + return 0; + } + if (x == null) + { + return 1; + } + if (y == null) + { + return -1; + } - if (handler is null) + // 先按长度比较,长度更长的排在前面。 + var lengthComparison = y.Length.CompareTo(x.Length); + if (lengthComparison != 0) { - throw new CommandNameNotFoundException( - $"No command handler found for command '{_commandLine.PossibleCommandNames}'. Please ensure that the command handler is registered correctly.", - _commandLine.PossibleCommandNames); + return lengthComparison; } - return handler.RunAsync(); + // 当长度相同时,按字典序比较。 + return caseSensitive + ? string.Compare(x, y, StringComparison.Ordinal) + : string.Compare(x, y, StringComparison.OrdinalIgnoreCase); } - - private readonly record struct CommandObjectCreationInfo(string? CommandNames, CommandObjectCreator Creator); } diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index 362e8afb..b1a55b5d 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -1,5 +1,7 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Utils.Handlers; namespace DotNetCampus.Cli; @@ -17,159 +19,155 @@ public static class CommandRunnerBuilderExtensions public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder) where T : class, ICommandHandler { - return builder.GetOrCreateRunner() - .AddHandler(); + throw MethodShouldBeInspected(); } - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 由拦截器传入的的命令处理器的命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator) - where T : class, ICommandHandler + /// + public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler) + where T : class { - return builder.GetOrCreateRunner() - .AddHandler(command, creator); + throw MethodShouldBeInspected(); } /// - public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler) + public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler) where T : class { - return builder.GetOrCreateRunner() - .AddHandler(t => - { - handler(t); - return Task.FromResult(0); - }); + throw MethodShouldBeInspected(); } /// [EditorBrowsable(EditorBrowsableState.Never)] - public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Action handler) + public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) where T : class { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, t => - { - handler(t); - return Task.FromResult(0); - }); + throw MethodShouldBeInspected(); } /// - public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler) + public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler) where T : class { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler); + throw MethodShouldBeInspected(); } /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Action handler) + public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) where T : class { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); + throw MethodShouldBeInspected(); } - /// - public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) + /// + /// 添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 用于处理已解析的命令行参数的委托。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 + public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler) where T : class { - return builder.GetOrCreateRunner() - .AddHandler(t => Task.FromResult(handler(t))); + throw MethodShouldBeInspected(); } - /// + /// + /// 由拦截器调用,用于添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 + /// 由拦截器传入的命令处理器创建方法。 + /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] - public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Func handler) - where T : class + public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) + where T : class, ICommandHandler { return builder.GetOrCreateRunner() - .AddHandler(command, creator, t => Task.FromResult(handler(t))); + .AddHandlerCore(command, creator, commandAliases); } /// - public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler) + [EditorBrowsable(EditorBrowsableState.Never)] + public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler); + return builder.GetOrCreateRunner() + .AddHandlerCore(command, cl => new AnonymousCommandHandler(cl, creator, handler), commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Func handler) + public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); + return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); } - /// - public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { return builder.GetOrCreateRunner() - .AddHandler(async t => - { - await handler(t); - return 0; - }); + .AddHandlerCore(command, cl => new AnonymousInt32CommandHandler(cl, creator, handler), commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Func handler) + public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, async t => - { - await handler(t); - return 0; - }); + return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); } - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { return builder.GetOrCreateRunner() - .AddHandler(handler); + .AddHandlerCore(command, cl => new AnonymousTaskCommandHandler(cl, creator, handler), commandAliases); } /// - /// 添加一个命令处理器。 + /// 由拦截器调用,用于添加一个命令处理器。 /// /// 命令行执行器构造的链式调用。 - /// 由拦截器传入的的命令处理器的命令名称。 - /// 由拦截器传入的命令处理器创建方法。 /// 用于处理已解析的命令行参数的委托。 + /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 + /// 由拦截器传入的命令处理器创建方法。 + /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, Func> handler) + public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler, + string? command, ExperimentalCommandObjectCreator creator, + IReadOnlyList? commandAliases = null + ) where T : class { return builder.GetOrCreateRunner() - .AddHandler(command, creator, handler); + .AddHandlerCore(command, cl => new AnonymousTaskInt32CommandHandler(cl, creator, handler), commandAliases); } /// @@ -181,6 +179,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu public static IAsyncCommandRunnerBuilder AddHandlers(this ICoreCommandRunnerBuilder builder) where T : ICommandHandlerCollection, new() { + throw new NotImplementedException(); return builder.GetOrCreateRunner() .AddHandlers(); } @@ -197,4 +196,10 @@ public static IAsyncCommandRunnerBuilder AddStandardHandlers(this ICoreCommandRu { throw new NotSupportedException("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature."); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static InvalidOperationException MethodShouldBeInspected() + { + return new InvalidOperationException("源生成器本应该在编译时拦截了此方法的调用。请检查编译警告,查看 DotNetCampus.CommandLine 的源生成器是否正常工作。"); + } } diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs b/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs index 5a8ce5fb..33f9e130 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs @@ -3,4 +3,9 @@ /// /// 从已解析的命令行参数创建命令数据模型或处理器的委托。 /// -public delegate object CommandObjectCreator(CommandLine commandLine); +public delegate object LegacyCommandObjectCreator(LegacyCommandLine commandLine); + +/// +/// 从已解析的命令行参数创建命令数据模型或处理器的委托。 +/// +public delegate object ExperimentalCommandObjectCreator(CommandLine commandLine); diff --git a/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs index f3d1987a..62723e38 100644 --- a/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs +++ b/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs @@ -18,7 +18,7 @@ public interface ICommandHandlerCollection /// /// 已解析的命令行参数。 /// 匹配的命令处理器,如果没有匹配的命令处理器,则返回 - ICommandHandler? TryMatch(string possibleCommandNames, CommandLine commandLine); + ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine); } internal static class CommandHandlerCollectionMatcher @@ -34,10 +34,10 @@ internal static class CommandHandlerCollectionMatcher /// 尝试匹配命令时,使用此集合中的命令处理器创建器。 /// 匹配的命令处理器,如果没有匹配的命令处理器,则返回 internal static ICommandHandler? TryMatch( - this CommandLine commandLine, + this LegacyCommandLine commandLine, string possibleCommandNames, - CommandObjectCreator? defaultHandlerCreator, - IReadOnlyDictionary commandHandlerCreators) + LegacyCommandObjectCreator? defaultHandlerCreator, + IReadOnlyDictionary commandHandlerCreators) { var caseSensitive = commandLine.ParsingOptions.CaseSensitive; if (string.IsNullOrEmpty(possibleCommandNames)) @@ -46,7 +46,7 @@ internal static class CommandHandlerCollectionMatcher } var bestMatchLength = -1; - var bestMatch = new KeyValuePair("", null!); + var bestMatch = new KeyValuePair("", null!); foreach (var pair in commandHandlerCreators) { var names = pair.Key; @@ -74,7 +74,7 @@ internal static class CommandHandlerCollectionMatcher if (isMatch && names.Length > bestMatchLength) { bestMatchLength = names.Length; - bestMatch = new KeyValuePair(names, creator); + bestMatch = new KeyValuePair(names, creator); } } return bestMatch.Value is { } handlerCreator diff --git a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs index b985f3ce..020d21b7 100644 --- a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs +++ b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs @@ -1,5 +1,41 @@ namespace DotNetCampus.Cli; +/// +/// 命令行执行器构造器,用于链式创建命令行执行器。 +/// +public interface ILegacyCoreCommandRunnerBuilder +{ + /// + /// 获取或创建一个命令行执行器。 + /// + /// 命令行执行器。 + internal LegacyCommandRunner GetOrCreateRunner(); +} + +/// +/// 命令行执行器构造器,用于链式创建命令行执行器。 +/// +public interface ILegacyCommandRunnerBuilder : ILegacyCoreCommandRunnerBuilder +{ + /// + /// 以同步方式运行命令行处理器。 + /// + /// 将被执行的命令行处理器的返回值。 + int Run(); +} + +/// +/// 命令行执行器构造器,用于链式创建命令行执行器。 +/// +public interface ILegacyAsyncCommandRunnerBuilder : ILegacyCoreCommandRunnerBuilder +{ + /// + /// 以异步方式运行命令行处理器。 + /// + /// 将被执行的命令行处理器的返回值。 + Task RunAsync(); +} + /// /// 命令行执行器构造器,用于链式创建命令行执行器。 /// diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLine.cs b/src/DotNetCampus.CommandLine/LegacyCommandLine.cs new file mode 100644 index 00000000..7100030a --- /dev/null +++ b/src/DotNetCampus.CommandLine/LegacyCommandLine.cs @@ -0,0 +1,397 @@ +using System.ComponentModel; +using System.Diagnostics.Contracts; +using System.Globalization; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Utils; +using DotNetCampus.Cli.Utils.Collections; + +namespace DotNetCampus.Cli; + +/// +/// 为应用程序提供统一的命令行参数解析功能。 +/// +public class LegacyCommandLine : ILegacyCoreCommandRunnerBuilder +{ + /// + /// 获取此命令行解析类型所关联的命令行参数。 + /// + public IReadOnlyList CommandLineArguments { get; } + + /// + /// 获取解析此命令行时所使用的各种选项。 + /// + internal CommandLineParsingOptions ParsingOptions { get; } + + /// + /// 在特定的属性不指定时,默认应使用的大小写敏感性。 + /// + public bool DefaultCaseSensitive { get; } + + /// + /// 获取命令行参数中猜测的多级命令名称。 + /// 请注意,此字符串中可能包含空格,表示多级命令名称。也可能比预期的更长,包含后续的一部分位置参数,因为暂时还无法确定那些位置参数是否是命令名称。 + /// + /// + /// + /// # 对于以下命令: + /// do something --option value + /// # 本属性的值为 "do something"。 + /// # 对于以下命令: + /// do something /var/file --option value + /// # 本属性的值为 "do something"(因为 /var/file 可以提前判断出来不可能是命令) + /// # 可能存在三种情况: + /// # 1. do 和 something 都是位置参数。 + /// # 2. do 是命令,something 是位置参数。 + /// # 3. do 和 something 都是命令。 + /// + /// 此属性保存这个 something 的值,待后续决定使用处理器时,根据处理器是否要求有命令来决定这个词是否是位置参数。
+ /// 另外,**特别强调**,此属性的值可能是命名变体,例如命令行传入 DoSomething 时,此属性则是 Do-Something。 + ///
+ internal string PossibleCommandNames { get; } + + /// + /// 如果此命令行是从 Web 请求的 URL 中解析出来的,则此属性保存 URL 的 Scheme 部分。 + /// + private string? MatchedUrlScheme { get; } + + /// + /// 适用于选项的多值处理方式。 + /// + private MultiValueHandling OptionMultiValueHandling { get; } + + /// + /// 适用于位置参数的多值处理方式。 + /// + private MultiValueHandling PositionalArgumentsMultiValueHandling { get; } + + /// + /// 从命令行中解析出来的长名称选项。始终大小写敏感。 + /// + private OptionDictionary LongOptionValuesDefault { get; } + + /// + /// 从命令行中解析出来的长名称选项。始终大小写敏感。 + /// + private OptionDictionary LongOptionValuesCaseSensitive { get; } + + /// + /// 从命令行中解析出来的长名称选项。始终大小写不敏感。 + /// + private OptionDictionary LongOptionValuesIgnoreCase { get; } + + /// + /// 从命令行中解析出来的短名称选项。始终大小写敏感。 + /// + private OptionDictionary ShortOptionValuesDefault { get; } + + /// + /// 从命令行中解析出来的短名称选项。始终大小写敏感。 + /// + private OptionDictionary ShortOptionValuesCaseSensitive { get; } + + /// + /// 从命令行中解析出来的短名称选项。始终大小写不敏感。 + /// + private OptionDictionary ShortOptionValuesIgnoreCase { get; } + + /// + /// 从命令行中解析出来的位置参数。 + /// + /// + /// 注意,位置参数的前几个值可能是命令名称;这取决于 和实际处理器的命令。 + /// + /// # 对于以下命令: + /// do something --option value + /// # 可能存在三种情况: + /// # 1. do 和 something 都是位置参数。 + /// # 2. do 是命令,something 是位置参数。 + /// # 3. do 和 something 都是命令。 + /// + /// 如果处理器决定将 something 作为命令名称,那么当需要取出位置参数时,此属性的第一个值需要排除。 + /// + private ReadOnlyListRange PositionalArguments { get; } + + private LegacyCommandLine() + { + var options = OptionDictionary.Empty; + var arguments = new ReadOnlyListRange(); + CommandLineArguments = arguments; + ParsingOptions = CommandLineParsingOptions.Flexible; + DefaultCaseSensitive = false; + PossibleCommandNames = ""; + MatchedUrlScheme = null; + OptionMultiValueHandling = MultiValueHandling.First; + PositionalArgumentsMultiValueHandling = MultiValueHandling.First; + LongOptionValuesCaseSensitive = options; + LongOptionValuesIgnoreCase = options; + LongOptionValuesDefault = options; + ShortOptionValuesCaseSensitive = options; + ShortOptionValuesIgnoreCase = options; + ShortOptionValuesDefault = options; + PositionalArguments = arguments; + } + + private LegacyCommandLine(IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions = null) + { + CommandLineArguments = arguments; + ParsingOptions = parsingOptions ?? CommandLineParsingOptions.Flexible; + DefaultCaseSensitive = parsingOptions?.CaseSensitive ?? false; + (MatchedUrlScheme, var result) = CommandLineConverter.ParseCommandLineArguments(arguments, parsingOptions); + PossibleCommandNames = result.PossibleCommandNames; + OptionMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.First : MultiValueHandling.Last; + PositionalArgumentsMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.SpaceAll : MultiValueHandling.SlashAll; + LongOptionValuesCaseSensitive = result.LongOptions.ToOptionLookup(true); + LongOptionValuesIgnoreCase = result.LongOptions.ToOptionLookup(false); + LongOptionValuesDefault = DefaultCaseSensitive ? LongOptionValuesCaseSensitive : LongOptionValuesIgnoreCase; + ShortOptionValuesCaseSensitive = result.ShortOptions.ToOptionLookup(true); + ShortOptionValuesIgnoreCase = result.ShortOptions.ToOptionLookup(false); + ShortOptionValuesDefault = DefaultCaseSensitive ? ShortOptionValuesCaseSensitive : ShortOptionValuesIgnoreCase; + PositionalArguments = result.Arguments; + } + + /// + /// 解析命令行参数,并获得一个通用的命令行解析类型。 + /// + /// 命令行参数。 + /// 以此方式解析命令行参数。 + /// 统一的命令行参数解析中间类型。 + [Pure] + public static LegacyCommandLine Parse(IReadOnlyList args, CommandLineParsingOptions? parsingOptions = null) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(args); +#else + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } +#endif + return args.Count is 0 + ? new LegacyCommandLine() + : new LegacyCommandLine(args, parsingOptions); + } + + /// + /// 解析一整行命令(所有参数被放在了同一个字符串中),并获得一个通用的命令行解析类型。 + /// + /// 一整行命令。 + /// 以此方式解析命令行参数。 + /// 统一的命令行参数解析中间类型。 + [Pure] + public static LegacyCommandLine Parse(string singleLineCommandLineArgs, CommandLineParsingOptions? parsingOptions = null) + { + var args = CommandLineConverter.SingleLineToList(singleLineCommandLineArgs); + return new LegacyCommandLine(args, parsingOptions); + } + + LegacyCommandRunner ILegacyCoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); + + /// + /// 尝试将命令行参数转换为指定类型的实例。 + /// + /// 要转换的类型。 + /// 转换后的实例。 + [Pure] + public T As() where T : class => LegacyCommandRunner.CreateInstance(this); + + /// + /// 尝试将命令行参数转换为指定类型的实例。 + /// + /// 由拦截器传入的命令处理器创建方法。 + /// 要转换的类型。 + /// 转换后的实例。 + [Pure, EditorBrowsable(EditorBrowsableState.Never)] + public T As(LegacyCommandObjectCreator creator) where T : class => LegacyCommandRunner.CreateInstance(this, creator); + + /// + /// 获取命令行参数中指定短名称的选项的值。 + /// + /// 短名称选项。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(char shortOption) => GetShortOption(shortOption.ToString(CultureInfo.InvariantCulture)); + + /// + /// 获取命令行参数中指定短名称的选项的值。 + /// + /// 短名称选项。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetShortOption(string shortOption) + { + return ShortOptionValuesDefault.TryGetValue(shortOption, out var defaultValues) + ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) + : null; + } + + /// + /// 获取命令行参数中指定名称的选项的值。 + /// + /// 选项的名称。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(string optionName) + { + return LongOptionValuesDefault.TryGetValue(optionName, out var defaultValues) + ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) + : null; + } + + /// + /// 获取命令行参数中指定名称的选项的值。 + /// + /// 短名称选项。 + /// 选项的名称。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(char shortName, string longName) => + // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 + GetOption(shortName) + // 其次使用长名称。 + ?? GetOption(longName); + + /// + /// 获取命令行参数中指定名称的选项的值。 + /// + /// 选项的名称。 + /// 单独为此选项设置的大小写敏感性。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(char optionName, bool caseSensitive) => + GetShortOption(optionName.ToString(CultureInfo.InvariantCulture), caseSensitive); + + /// + /// 获取命令行参数中指定短名称的选项的值。 + /// + /// 短名称选项。 + /// 单独为此选项设置的大小写敏感性。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetShortOption(string shortOption, bool caseSensitive) + { + var optionValues = caseSensitive + ? ShortOptionValuesCaseSensitive + : ShortOptionValuesIgnoreCase; + return optionValues.TryGetValue(shortOption, out var defaultValues) + ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) + : null; + } + + /// + /// 获取命令行参数中指定名称的选项的值。 + /// + /// 选项的名称。 + /// 单独为此选项设置的大小写敏感性。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(string optionName, bool caseSensitive) + { + var optionValues = caseSensitive + ? LongOptionValuesCaseSensitive + : LongOptionValuesIgnoreCase; + return optionValues.TryGetValue(optionName, out var defaultValues) + ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) + : null; + } + + /// + /// 获取命令行参数中指定名称的选项的值。 + /// + /// 短名称选项。 + /// 选项的名称。 + /// 单独为此选项设置的大小写敏感性。 + /// 返回选项的值。当命令行未传入此参数时返回 + [Pure] + public CommandLinePropertyValue? GetOption(char shortName, string longName, bool caseSensitive) => + // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 + GetOption(shortName, caseSensitive) + // 其次使用长名称。 + ?? GetOption(longName, caseSensitive); + + /// + /// 获取命令行参数中位置参数的值。 + /// + /// 获取指定索引处的参数值。 + /// 从索引处获取参数值的最长长度。当大于 1 时,会将这些值合并为一个字符串。 + /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 + /// 位置参数的值。 + [Pure] + public CommandLinePropertyValue? GetPositionalArgument(int index, int length, string? commandNames = null) + { + var commandLevel = GetCommandLevel(commandNames); + var positionalArgumentsStartIndex = Math.Max(0, commandLevel); + positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); + var realIndex = index + positionalArgumentsStartIndex; + return realIndex < 0 || realIndex >= PositionalArguments.Count + ? null + : new CommandLinePropertyValue( + PositionalArguments.Slice(realIndex, + Math.Min(length, PositionalArguments.Count - realIndex)), PositionalArgumentsMultiValueHandling); + } + + /// + /// 获取命令行参数中所有位置参数值的集合。 + /// + /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 + /// 命令行参数中位置参数值的集合。 + [Pure] + public IReadOnlyList GetPositionalArguments(string? commandNames = null) + { + var commandLevel = GetCommandLevel(commandNames); + var positionalArgumentsStartIndex = Math.Max(0, commandLevel); + positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); + return PositionalArguments.Slice(positionalArgumentsStartIndex, PositionalArguments.Count - 1); + } + + /// + /// 根据某个特定的命令名称字符串,获取此字符串中包含了多少级命令。 + /// + /// 命令名称字符串。 + /// 命令的层级数。 + private int GetCommandLevel(string? commandNames) + { + var possibleCommandNames = PossibleCommandNames; + + // 如果没有命令,则不需要排除任何位置参数。 + if (string.IsNullOrEmpty(commandNames) || string.IsNullOrEmpty(possibleCommandNames)) + { + return 0; + } +#if !NETCOREAPP3_1_OR_GREATER + if (commandNames is null) + { + return 0; + } +#endif + if (commandNames.Length > possibleCommandNames.Length) + { + return 0; + } + if (!possibleCommandNames.StartsWith(commandNames, DefaultCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + // 计算 possibleCommandNames 中有多少个空格。 + var commandLevel = 1; + for (var i = 0; i < commandNames.Length; i++) + { + if (possibleCommandNames[i] == ' ') + { + commandLevel++; + } + } + return commandLevel; + } + + /// + /// 输出传入的命令行参数字符串。 + /// + /// 传入的命令行参数字符串。 + [Pure] + public override string ToString() + { + return MatchedUrlScheme is { } scheme + ? $"{scheme}://{string.Join("/", PositionalArguments)}?{string.Join("&", LongOptionValuesCaseSensitive.Select(x => $"{x.Key}={string.Join("&", x.Value)}"))}" + : string.Join(" ", CommandLineArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); + } +} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs b/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs new file mode 100644 index 00000000..c1e414ed --- /dev/null +++ b/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs @@ -0,0 +1,212 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Handlers; + +namespace DotNetCampus.Cli; + +/// +/// 辅助 根据已解析的命令行参数执行对应的命令处理器。 +/// +public class LegacyCommandRunner : ILegacyCommandRunnerBuilder, ILegacyAsyncCommandRunnerBuilder +{ + private static ConcurrentDictionary CommandObjectCreationInfos { get; } = new( +#if NET5_0_OR_GREATER + ReferenceEqualityComparer.Instance +#endif + ); + + private readonly LegacyCommandLine _commandLine; + private readonly DictionaryCommandHandlerCollection _dictionaryCommandHandlers = new(); + private readonly ConcurrentDictionary _assemblyCommandHandlers = []; + + internal LegacyCommandRunner(LegacyCommandLine commandLine) + { + _commandLine = commandLine; + } + + internal LegacyCommandRunner(LegacyCommandRunner commandRunner) + { + _commandLine = commandRunner._commandLine; + } + + /// + /// 供源生成器调用,注册一个专门用来处理主命令(Main Command)或子命令/多级子命令(Sub Command)的命令处理器。 + /// + /// 关联的命令。 + /// 命令处理器的创建方法。 + /// 选项类型,或命令处理器类型,或任意类型。 + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Register(string? commandNames, LegacyCommandObjectCreator creator) + where T : class + { + CommandObjectCreationInfos[typeof(T)] = new CommandObjectCreationInfo(commandNames, creator); + } + + /// + /// 创建一个命令处理器实例。 + /// + /// 已解析的命令行参数。 + /// 命令处理器的类型。 + /// 命令处理器实例。 + internal static T CreateInstance(LegacyCommandLine commandLine) + { + if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + { + throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + } + + return (T)info.Creator(commandLine); + } + + /// + /// 创建一个命令处理器实例。 + /// + /// 已解析的命令行参数。 + /// 命令处理器的创建方法。 + /// 命令处理器的类型。 + /// 命令处理器实例。 + internal static T CreateInstance(LegacyCommandLine commandLine, LegacyCommandObjectCreator creator) + { + return (T)creator(commandLine); + } + + LegacyCommandRunner ILegacyCoreCommandRunnerBuilder.GetOrCreateRunner() => this; + + /// + /// 添加一个命令处理器。 + /// + /// 命令处理器的类型。 + /// 返回一个命令处理器构建器。 + internal LegacyCommandRunner AddHandler() + where T : class, ICommandHandler + { + if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + { + throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + } + + _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => (T)info.Creator(cl)); + return this; + } + + /// + /// 添加一个命令处理器。 + /// + /// 由拦截器传入的的命令处理器的命令。 + /// 由拦截器传入的命令处理器创建方法。 + /// 命令处理器的类型。 + /// 返回一个命令处理器构建器。 + [EditorBrowsable(EditorBrowsableState.Never)] + internal LegacyCommandRunner AddHandler(string? command, LegacyCommandObjectCreator creator) + where T : class, ICommandHandler + { + _dictionaryCommandHandlers.AddHandler(command, creator); + return this; + } + + /// + /// 添加一个命令处理器。 + /// + /// 用于处理已解析的命令行参数的委托。 + /// 命令处理器的类型。 + /// 返回一个命令处理器构建器。 + internal LegacyCommandRunner AddHandler(Func> handler) + where T : class + { + if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) + { + throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); + } + + _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => new TaskCommandHandler( + () => (T)info.Creator(cl), + handler)); + return this; + } + + /// + /// 添加一个命令处理器。 + /// + /// 由拦截器传入的的命令处理器的命令。 + /// 由拦截器传入的命令处理器创建方法。 + /// 用于处理已解析的命令行参数的委托。 + /// 命令处理器的类型。 + /// 返回一个命令处理器构建器。 + internal LegacyCommandRunner AddHandler(string? command, LegacyCommandObjectCreator creator, Func> handler) + where T : class + { + _dictionaryCommandHandlers.AddHandler(command, cl => new TaskCommandHandler( + () => (T)creator(cl), + handler)); + return this; + } + + internal LegacyCommandRunner AddHandlers() + where T : ICommandHandlerCollection, new() + { + var c = new T(); + _assemblyCommandHandlers.TryAdd(c, c); + return this; + } + + private ICommandHandler? MatchHandler() + { + var possibleCommandNames = _commandLine.PossibleCommandNames; + + // 优先寻找单独添加的处理器。 + if (_dictionaryCommandHandlers.TryMatch(possibleCommandNames, _commandLine) is { } h1) + { + return h1; + } + + // 其次寻找程序集中自动搜集到的处理器。 + foreach (var handler in _assemblyCommandHandlers) + { + if (handler.Value.TryMatch(possibleCommandNames, _commandLine) is { } h2) + { + return h2; + } + } + + // 如果没有找到,那么很可能此命令没有命令名称,需要使用默认的处理器。 + if (_dictionaryCommandHandlers.TryMatch("", _commandLine) is { } h3) + { + return h3; + } + foreach (var handler in _assemblyCommandHandlers) + { + if (handler.Value.TryMatch("", _commandLine) is { } h4) + { + return h4; + } + } + + // 如果连默认的处理器都没有找到,说明根本没有能处理此命令的处理器。 + return null; + } + + /// + public int Run() + { + return RunAsync().Result; + } + + /// + public Task RunAsync() + { + var handler = MatchHandler(); + + if (handler is null) + { + throw new CommandNameNotFoundException( + $"No command handler found for command '{_commandLine.PossibleCommandNames}'. Please ensure that the command handler is registered correctly.", + _commandLine.PossibleCommandNames); + } + + return handler.RunAsync(); + } + + private readonly record struct CommandObjectCreationInfo(string? CommandNames, LegacyCommandObjectCreator Creator); +} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs new file mode 100644 index 00000000..e5e01670 --- /dev/null +++ b/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs @@ -0,0 +1,200 @@ +using System.ComponentModel; +using DotNetCampus.Cli.Compiler; + +namespace DotNetCampus.Cli; + +/// +/// 辅助创建命令行执行程序。 +/// +public static class LegacyCommandRunnerBuilderExtensions +{ + /// + /// 添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder) + where T : class, ICommandHandler + { + return builder.GetOrCreateRunner() + .AddHandler(); + } + + /// + /// 添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 由拦截器传入的的命令处理器的命令名称。 + /// 由拦截器传入的命令处理器创建方法。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator) + where T : class, ICommandHandler + { + return builder.GetOrCreateRunner() + .AddHandler(command, creator); + } + + /// + public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Action handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(t => + { + handler(t); + return Task.FromResult(0); + }); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Action handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(command, creator, t => + { + handler(t); + return Task.FromResult(0); + }); + } + + /// + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, Action handler) + where T : class + { + return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(handler); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Action handler) + where T : class + { + return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); + } + + /// + public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(t => Task.FromResult(handler(t))); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Func handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(command, creator, t => Task.FromResult(handler(t))); + } + + /// + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, Func handler) + where T : class + { + return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(handler); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Func handler) + where T : class + { + return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); + } + + /// + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(async t => + { + await handler(t); + return 0; + }); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Func handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(command, creator, async t => + { + await handler(t); + return 0; + }); + } + + /// + /// 添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 用于处理已解析的命令行参数的委托。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func> handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(handler); + } + + /// + /// 添加一个命令处理器。 + /// + /// 命令行执行器构造的链式调用。 + /// 由拦截器传入的的命令处理器的命令名称。 + /// 由拦截器传入的命令处理器创建方法。 + /// 用于处理已解析的命令行参数的委托。 + /// 命令处理器的类型。 + /// 命令行执行器构造的链式调用。 + [EditorBrowsable(EditorBrowsableState.Never)] + public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, + string? command, LegacyCommandObjectCreator creator, Func> handler) + where T : class + { + return builder.GetOrCreateRunner() + .AddHandler(command, creator, handler); + } + + /// + /// 添加一个命令处理器集合。 + /// + /// 命令行执行器构造的链式调用。 + /// 命令处理器集合的类型。 + /// 命令行执行器构造的链式调用。 + public static ILegacyAsyncCommandRunnerBuilder AddHandlers(this ILegacyCoreCommandRunnerBuilder builder) + where T : ICommandHandlerCollection, new() + { + return builder.GetOrCreateRunner() + .AddHandlers(); + } + + /// + /// 添加支持 GNU 标准的命令行通用参数。这将在无参数,带 --help 参数和带 --version 参数时得到通用的响应。
+ /// 考虑到几乎没有开发者认为这个方法的行为符合预期,我们移除了这个功能。 + ///
+ /// 命令行执行器构造的链式调用。 + /// 命令行执行器构造的链式调用。 + /// 任何时候调用这个方法都会抛出这个异常。 + [Obsolete("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature.", true)] + public static ILegacyAsyncCommandRunnerBuilder AddStandardHandlers(this ILegacyCoreCommandRunnerBuilder builder) + { + throw new NotSupportedException("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature."); + } +} diff --git a/src/DotNetCampus.CommandLine/Properties/Compatibility.cs b/src/DotNetCampus.CommandLine/Properties/Compatibility.cs new file mode 100644 index 00000000..fdd37fb4 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Properties/Compatibility.cs @@ -0,0 +1,27 @@ +global using DotNetCampus.Cli.Properties; + +namespace DotNetCampus.Cli.Properties; + +internal static class CompatibilityExtensionMethods +{ +#if NETCOREAPP3_1_OR_GREATER +#else + internal static void Deconstruct(this KeyValuePair pair, out TKey key, out TValue value) + where TKey : notnull + { + key = pair.Key; + value = pair.Value; + } + + internal static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + where TKey : notnull + { + if (dictionary.ContainsKey(key)) + { + return false; + } + dictionary.Add(key, value); + return true; + } +#endif +} diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 669e3a6c..97afc1b7 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -12,7 +12,7 @@ internal static class CommandLineConverter ///
/// 一整行命令。 /// 命令行参数数组。 - internal static IReadOnlyList SingleLineCommandLineArgsToArrayCommandLineArgs(string singleLineCommandLineArgs) + internal static IReadOnlyList SingleLineToList(string singleLineCommandLineArgs) { if (string.IsNullOrWhiteSpace(singleLineCommandLineArgs)) { diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs index 17330411..5c4d9cd1 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs @@ -5,10 +5,10 @@ namespace DotNetCampus.Cli.Utils.Handlers; internal sealed class DictionaryCommandHandlerCollection : ICommandHandlerCollection { - private CommandObjectCreator? _defaultHandlerCreator; - private readonly ConcurrentDictionary _commandHandlerCreators = []; + private LegacyCommandObjectCreator? _defaultHandlerCreator; + private readonly ConcurrentDictionary _commandHandlerCreators = []; - public void AddHandler(string? commandNames, CommandObjectCreator handlerCreator) + public void AddHandler(string? commandNames, LegacyCommandObjectCreator handlerCreator) { if ( #if !NETCOREAPP3_1_OR_GREATER @@ -31,7 +31,7 @@ commandNames is null || } } - public ICommandHandler? TryMatch(string possibleCommandNames, CommandLine commandLine) + public ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine) { return commandLine.TryMatch(possibleCommandNames, _defaultHandlerCreator, _commandHandlerCreators); } diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs index 88e1ccd6..5e81e16a 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs @@ -10,15 +10,15 @@ public abstract class GeneratedAssemblyCommandHandlerCollection : ICommandHandle /// /// 源生成器在构造函数中,为没有命令名称的命令处理器赋值。 /// - protected CommandObjectCreator? Default { get; init; } + protected LegacyCommandObjectCreator? Default { get; init; } /// /// 源生成器在构造函数中,为有命令名称的命令处理器赋值。 /// - protected Dictionary Creators { get; init; } = []; + protected Dictionary Creators { get; init; } = []; /// - public ICommandHandler? TryMatch(string possibleCommandNames, CommandLine commandLine) + public ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine) { return commandLine.TryMatch(possibleCommandNames, Default, Creators); } diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 8afc7984..f8441141 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -1,3 +1,5 @@ +using DotNetCampus.Cli.Compiler; + namespace DotNetCampus.Cli.Utils.Handlers; internal sealed class TaskCommandHandler( @@ -14,7 +16,85 @@ public Task RunAsync() { throw new InvalidOperationException($"No options of type {typeof(TOptions)} were created."); } + return handler(_options); + } +} + +internal sealed class AnonymousCommandHandler( + CommandLine commandLine, + ExperimentalCommandObjectCreator creator, + Action handler) : ICommandHandler + where T : class +{ + private T? _options; + + public Task RunAsync() + { + _options ??= (T)creator(commandLine); + if (_options is null) + { + throw new InvalidOperationException($"No options of type {typeof(T)} were created."); + } + handler(_options); + return Task.FromResult(0); + } +} + +internal sealed class AnonymousInt32CommandHandler( + CommandLine commandLine, + ExperimentalCommandObjectCreator creator, + Func handler) : ICommandHandler + where T : class +{ + private T? _options; + + public Task RunAsync() + { + _options ??= (T)creator(commandLine); + if (_options is null) + { + throw new InvalidOperationException($"No options of type {typeof(T)} were created."); + } + handler(_options); + return Task.FromResult(0); + } +} + +internal sealed class AnonymousTaskCommandHandler( + CommandLine commandLine, + ExperimentalCommandObjectCreator creator, + Func handler) : ICommandHandler + where T : class +{ + private T? _options; + + public async Task RunAsync() + { + _options ??= (T)creator(commandLine); + if (_options is null) + { + throw new InvalidOperationException($"No options of type {typeof(T)} were created."); + } + await handler(_options); + return 0; + } +} + +internal sealed class AnonymousTaskInt32CommandHandler( + CommandLine commandLine, + ExperimentalCommandObjectCreator creator, + Func> handler) : ICommandHandler + where T : class +{ + private T? _options; + public Task RunAsync() + { + _options ??= (T)creator(commandLine); + if (_options is null) + { + throw new InvalidOperationException($"No options of type {typeof(T)} were created."); + } return handler(_options); } } From 3cf6606eb2e5a73677a2c67ed5b3ef9b04fa8932 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 13 Sep 2025 17:37:44 +0800 Subject: [PATCH 002/193] =?UTF-8?q?=E5=BC=80=E5=A7=8B=E7=9D=80=E6=89=8B?= =?UTF-8?q?=E7=BC=96=E5=86=99=E5=B0=BD=E5=8F=AF=E8=83=BD=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E5=88=86=E9=85=8D=E7=9A=84=E5=91=BD=E4=BB=A4=E8=A1=8C=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E6=89=80=E9=9C=80=E7=9A=84=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandLine.cs | 36 +- .../CommandLineParsingOptions.cs | 307 ++++++++++++++---- .../CommandLineStyle.cs | 279 ++++------------ src/DotNetCampus.CommandLine/CommandRunner.cs | 6 +- .../CommandRunnerBuilderExtensions.cs | 73 ++--- .../CommandSeparatorChars.cs | 104 ++++++ .../Compiler/CommandObjectCreator.cs | 2 +- .../Compiler/PropertyAssignments.cs | 65 ++++ .../LegacyCommandLine.cs | 12 +- .../LegacyCommandLineParsingOptions.cs | 106 ++++++ .../LegacyCommandLineStyle.cs | 252 ++++++++++++++ .../Utils/BooleanValues32.cs | 73 +++++ .../Utils/CommandLineConverter.cs | 12 +- .../Utils/Handlers/TaskCommandHandler.cs | 16 +- .../Utils/Parsers/DotNetStyleParser.cs | 2 +- .../Utils/Parsers/FlexibleStyleParser.cs | 2 +- .../Utils/Parsers/GenericStyleParser.cs | 91 ++++++ .../Utils/Parsers/GnuStyleParser.cs | 2 +- .../Utils/Parsers/PosixStyleParser.cs | 2 +- .../Utils/Parsers/PowerShellStyleParser.cs | 2 +- 20 files changed, 1081 insertions(+), 363 deletions(-) create mode 100644 src/DotNetCampus.CommandLine/CommandSeparatorChars.cs create mode 100644 src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs create mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs create mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs create mode 100644 src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index 1fb4ada8..f614b462 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -1,4 +1,7 @@ +using System.ComponentModel; using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; namespace DotNetCampus.Cli; @@ -16,12 +19,12 @@ public class CommandLine : ICoreCommandRunnerBuilder /// /// 获取解析此命令行时所使用的各种选项。 /// - internal CommandLineParsingOptions ParsingOptions { get; } + public CommandLineParsingOptions ParsingOptions { get; } /// /// 在特定的属性不指定时,默认应使用的大小写敏感性。 /// - public bool DefaultCaseSensitive => ParsingOptions.CaseSensitive; + public bool DefaultCaseSensitive => ParsingOptions.Style.CaseSensitive; private CommandLine() { @@ -70,5 +73,34 @@ public static CommandLine Parse(string singleLineCommandLineArgs, CommandLinePar return new CommandLine(args, parsingOptions); } + /// + /// 尝试将命令行参数转换为指定类型的实例。 + /// + /// 要转换的类型。 + /// 转换后的实例。 + [Pure] + public T As() where T : notnull => throw MethodShouldBeInspected(); + + /// + /// 尝试将命令行参数转换为指定类型的实例。 + /// + /// 由拦截器传入的命令处理器创建方法。 + /// 要转换的类型。 + /// 转换后的实例。 + [Pure, EditorBrowsable(EditorBrowsableState.Never)] + public T As(CommandObjectCreator creator) where T : notnull + { + return (T)creator(this); + } + CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); + + /// + /// 当某个方法本应该被源生成器拦截时,却仍然被调用了,就调用此方法抛出异常。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static InvalidOperationException MethodShouldBeInspected() + { + return new InvalidOperationException("源生成器本应该在编译时拦截了此方法的调用。请检查编译警告,查看 DotNetCampus.CommandLine 的源生成器是否正常工作。"); + } } diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index e3752d34..46728d78 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -1,106 +1,275 @@ +using DotNetCampus.Cli.Utils; + namespace DotNetCampus.Cli; /// /// 在解析命令行参数时,指定命令行参数的解析方式。 /// -public readonly record struct CommandLineParsingOptions() +public readonly record struct CommandLineParsingOptions { /// public static CommandLineParsingOptions Flexible => new CommandLineParsingOptions { - Style = CommandLineStyle.Flexible, - CaseSensitive = false, + Style = new CommandLineStyleDetails + { + CaseSensitive = false, + SupportsLongOption = true, + SupportsShortOption = true, + NamingPolicy = CommandNamingPolicy.Both, + OptionPrefix = CommandOptionPrefix.DoubleDash, + OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + DictionaryValueSeparators = CommandSeparatorChars.Create('='), + }, + }; + + /// + public static CommandLineParsingOptions DotNet => new CommandLineParsingOptions + { + Style = new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = true, + SupportsShortOption = true, + NamingPolicy = CommandNamingPolicy.KebabCase, + OptionPrefix = CommandOptionPrefix.DoubleDash, + OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + DictionaryValueSeparators = CommandSeparatorChars.Create('='), + }, }; /// public static CommandLineParsingOptions Gnu => new CommandLineParsingOptions { - Style = CommandLineStyle.Gnu, - CaseSensitive = true, + Style = new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = true, + SupportsShortOption = true, + NamingPolicy = CommandNamingPolicy.KebabCase, + OptionPrefix = CommandOptionPrefix.DoubleDash, + OptionValueSeparators = CommandSeparatorChars.Create('=', ' '), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + DictionaryValueSeparators = CommandSeparatorChars.Create('='), + }, }; /// public static CommandLineParsingOptions Posix => new CommandLineParsingOptions { - Style = CommandLineStyle.Posix, - CaseSensitive = true, - }; - - /// - public static CommandLineParsingOptions DotNet => new CommandLineParsingOptions - { - Style = CommandLineStyle.DotNet, - CaseSensitive = false, + Style = new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = false, + SupportsShortOption = true, + NamingPolicy = CommandNamingPolicy.CamelCase, + OptionPrefix = CommandOptionPrefix.SingleDash, + OptionValueSeparators = CommandSeparatorChars.Create(' '), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), + DictionaryValueSeparators = CommandSeparatorChars.Create('='), + }, }; /// public static CommandLineParsingOptions PowerShell => new CommandLineParsingOptions { - Style = CommandLineStyle.PowerShell, - CaseSensitive = false, + Style = new CommandLineStyleDetails + { + CaseSensitive = false, + SupportsLongOption = true, + SupportsShortOption = true, + NamingPolicy = CommandNamingPolicy.PascalCase, + OptionPrefix = CommandOptionPrefix.Slash, + OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + DictionaryValueSeparators = CommandSeparatorChars.Create('='), + }, }; /// - /// 以此风格解析命令行参数。 + /// 详细设置命令行解析时的各种细节。 /// - /// - /// 不指定时会自动根据用户输入的命令行参数判断风格。 - /// - public CommandLineStyle Style { get; init; } + public CommandLineStyleDetails Style { get; init; } +} + +/// +/// 详细指定一种命令行风格的细节。 +/// +public readonly record struct CommandLineStyleDetails() +{ + private readonly BooleanValues32 _booleans; /// - /// 默认是大小写不敏感的,设置此值为 可以让命令行参数大小写敏感。 + /// 直接由程序员提前算好各种属性赋值完成后的魔数,节省应用程序启动期间的额外计算。 /// - /// - /// 当然,可以在单独的属性上设置大小写敏感,设置后将在那个属性上覆盖此默认值。不设置的属性会使用此默认值。 - /// - public bool CaseSensitive { get; init; } + /// 魔数。 + internal CommandLineStyleDetails(int magic) : this() + { + _booleans = new BooleanValues32(magic); + } + + /// + /// 允许用户在命令行中使用的命令行参数风格。 + /// + public CommandNamingPolicy NamingPolicy + { + // [0] 表示是否额外编译时转换以支持 PascalCase/CamelCase 命名法 + // [1] 表示原样大小写,还是编译时按命名法转小写 + // [2] 表示是否同时支持 kebab-case 和 PascalCase/CamelCase 命名法 + get => _booleans[0, 1, 2] switch + { + (true, true, false) => CommandNamingPolicy.PascalCase, + (true, false, false) => CommandNamingPolicy.CamelCase, + (false, true, false) => CommandNamingPolicy.KebabCase, + (false, false, false) => CommandNamingPolicy.KebabCaseLower, + (true, true, true) => CommandNamingPolicy.Both, + (true, false, true) => CommandNamingPolicy.BothLower, + _ => throw new InvalidOperationException("Invalid naming policy."), + }; + init => _booleans[0, 1, 2] = value switch + { + CommandNamingPolicy.PascalCase => (true, true, false), + CommandNamingPolicy.CamelCase => (true, false, false), + CommandNamingPolicy.KebabCase => (false, true, false), + CommandNamingPolicy.KebabCaseLower => (false, false, false), + CommandNamingPolicy.Both => (true, true, true), + CommandNamingPolicy.BothLower => (true, false, true), + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), + }; + } + + /// + /// 指定命令行选项前缀的风格。 + /// + public CommandOptionPrefix OptionPrefix + { + // [3] 表示是否使用短横线(-)作为选项前缀 + // [4] 表示长选项是否使用双短横线(--) + get => _booleans[3, 4]switch + { + (true, true) => CommandOptionPrefix.DoubleDash, + (true, false) => CommandOptionPrefix.SingleDash, + (false, _) => CommandOptionPrefix.Slash, + }; + init => _booleans[3, 4] = value switch + { + CommandOptionPrefix.DoubleDash => (true, true), + CommandOptionPrefix.SingleDash => (true, false), + CommandOptionPrefix.Slash => (false, false), + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), + }; + } + + /// + /// 在单独的选项没有特别指定时,默认是否区分大小写。 + /// + public bool CaseSensitive + { + get => _booleans[5]; + init => _booleans[5] = value; + } + + /// + /// 是否支持长选项。 + /// + public bool SupportsLongOption + { + get => _booleans[6]; + init => _booleans[6] = value; + } + + /// + /// 是否支持短选项。 + /// + public bool SupportsShortOption + { + get => _booleans[7]; + init => _booleans[7] = value; + } /// - /// 此命令行解析器支持从 Web 打开本地应用时传入的参数。
- /// 此属性指定用于 URI 协议注册的方案名(scheme name)。 + /// 允许用户使用哪些分隔符来分隔选项名和选项值。
+ /// 如 ':', '=', ' ' 分别对应: --option:value, --option=value, --option value。 ///
/// - /// - /// 例如:sample://open?url=DotNetCampus%20is%20a%20great%20team
- /// 这里的 "sample" 就是方案名。
- /// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 - ///
- /// /// - /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
- ///
- /// 详细规则:
- /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
- /// 2. 参数部分以问号(?)开始,后面是键值对
- /// 3. 多个参数之间用(&)符号分隔
- /// 4. 每个参数的键值之间用等号(=)分隔
- /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
- /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
- /// 7. 支持无值参数,被视为布尔值true,如?enabled
- /// 8. 参数值为空字符串时保留等号,如?name=
- /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
- /// 10. 但在某些情况下,路径的前几个部分可能会被当作命令(含子命令),例如 myapp://open/file.txt 中,open 可能是命令,file.txt 是位置参数。具体解释为位置参数还是命令取决于应用的命令行处理器实现
- /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
- ///
- /// - /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) - /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 - /// myapp://user/profile?id=123&tab=info # 带层级路径 - /// sample://document/edit?id=42&mode=full # 多参数和路径组合 - /// - /// # 特殊字符与编码 - /// yourapp://search?q=hello%20world # 编码空格 - /// myapp://open?query=C%23%20programming # 特殊字符编码 - /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) - /// - /// # 无值和空值参数 - /// myapp://settings?debug # 无值参数(视为true) - /// yourapp://profile?name=&id=123 # 空字符串值 - /// - /// # 路径与命令示例 - /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 - /// myapp://open/file.txt?temporary=true # open 是命令,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 - /// + /// 如果指定空格(' '),则表示选项名和选项值之间可以用空格分隔,如 --option value。
+ /// 如果指定冒号(':'),则表示选项名和选项值之间可以用冒号分隔,如 --option:value。
+ /// 如果指定等号('='),则表示选项名和选项值之间可以用等号分隔,如 --option=value。
+ /// 而如果指定为空字符('\0'),则此字符只会对短选项生效,表示短选项可以直接跟值,如 -oValue。毕竟长选项跟值也分不开,对吧!
+ /// 基本上不会再存在其他种类的分隔符了…… ///
- public IReadOnlyList SchemeNames { get; init; } = []; + public CommandSeparatorChars OptionValueSeparators { get; init; } + + /// + /// 允许用户使用哪些分隔符来分隔集合类型的选项值。
+ /// 如 ',', ';', ' ' 分别对应: --option value1,value2, --option value1;value2, --option value1 value2。 + ///
+ public CommandSeparatorChars CollectionValueSeparators { get; init; } + + /// + /// 允许用户使用哪些分隔符来分隔字典类型的选项值中的键和值。
+ /// 如 '=', ':' 分别对应: --option key=value, --option key:value。 + ///
+ public CommandSeparatorChars DictionaryValueSeparators { get; init; } +} + +/// +/// 允许用户在命令行中使用的命令和选项的命名风格。 +/// +/// +/// 虽然在不区分大小写时, 看起来是一样的,但在输出帮助文档时会以设定的为准。 +/// +public enum CommandNamingPolicy +{ + /// + /// PascalCase 风格命名。 + /// + PascalCase, + + /// + /// camelCase 风格命名。 + /// + CamelCase, + + /// + /// kebab-case 风格命名,保持原样大小写。 + /// + KebabCase, + + /// + /// kebab-case 风格命名,且所有字母均为小写。 + /// + KebabCaseLower, + + /// + /// 以 kebab-case 命名风格为主,兼顾支持 PascalCase。 + /// + Both, + + /// + /// 以 kebab-case 命名风格为主(所有字母均为小写),兼顾支持 PascalCase 和 camelCase。 + /// + BothLower, +} + +/// +/// 指定命令行选项前缀的风格。 +/// +public enum CommandOptionPrefix +{ + /// + /// 使用双短横线(--)作为长选项前缀,使用单个短横线(-)作为短选项前缀。 + /// + DoubleDash, + + /// + /// 使用单个短横线(-)作为长选项和短选项前缀。 + /// + SingleDash, + + /// + /// 使用斜杠(/)作为长选项和短选项前缀。 + /// + Slash, } diff --git a/src/DotNetCampus.CommandLine/CommandLineStyle.cs b/src/DotNetCampus.CommandLine/CommandLineStyle.cs index 58f0c29f..7a826f65 100644 --- a/src/DotNetCampus.CommandLine/CommandLineStyle.cs +++ b/src/DotNetCampus.CommandLine/CommandLineStyle.cs @@ -1,4 +1,4 @@ -namespace DotNetCampus.Cli; +namespace DotNetCampus.Cli; /// /// 命令行参数的风格规范。 @@ -8,245 +8,78 @@ public enum CommandLineStyle { /// /// 灵活风格。
- /// 根据实际传入的参数,自动识别并支持多种主流风格,包括 等风格。 - /// 适用于希望为用户提供更灵活的参数传递体验的工具。 + /// 在绝大多数情况下,接受用户输入各种风格的命令行参数(包括 等);
+ /// 此风格给了用户最大的灵活性。但同时,作为开发者,你定义命令行选项时也应该尽可能避免不同风格间可能出现的歧义。
+ /// 当然,绝大多数情况下,你都不会碰到可能歧义的情况。 ///
/// - /// 灵活风格是一种包容性最强的命令行参数风格,旨在让不熟悉命令行操作的用户也能轻松使用。它通过智能识别尝试理解用户输入的意图,支持多种参数格式共存。
- ///
- /// 详细规则:
- /// 1. 参数前缀支持多种形式:双破折线(--), 单破折线(-), 斜杠(/,仅 Windows)
- /// 2. 参数值分隔符兼容多种形式:空格、等号(=)、冒号(:)
- /// 3. 参数命名风格兼容kebab-case(--parameter-name)、PascalCase(-ParameterName)和camelCase
- /// 4. 默认大小写不敏感,便于初学者使用
- /// 5. 支持短选项(-a)和长选项(--parameter),优先识别长选项
- /// 6. 支持布尔开关参数,可不带值或使用true/false、yes/no、on/off等常见值
- /// 7. 支持位置参数,并可通过双破折号(--)标记位置参数的开始
- /// 8. 支持有限的短选项组合(-abc),但当发生歧义时优先解析为单个选项
- /// 9. 当特性之间发生冲突时,优先保留简单、直观的用法,牺牲高级但复杂的功能
- /// 10. 自动检测并处理常见的用户错误,如选项名称拼写错误提示最接近的选项
- /// 11. 允许不同风格在同一命令行中混合使用
- ///
- /// 不支持的特性(为避免冲突):
- /// 1. 短选项组合中的最后一个选项不能直接附带参数(如-abc value,c无法接收value作为参数)
- /// 2. 不支持POSIX风格中的特殊数字操作数形式(如-42表示数字42)
- ///
- /// - /// # 长选项示例(多种风格) - /// app --parameter value # GNU风格空格分隔 - /// app --parameter=value # GNU风格等号分隔 - /// app --parameter:value # DotNet风格冒号分隔 - /// app -Parameter value # PowerShell风格(Pascal命名) - /// app --param-name value # Kebab-case命名 - /// app --paramName value # CamelCase命名 - /// - /// # 短选项示例(兼容多种形式) - /// app -p value # 短选项空格分隔 - /// app -p=value # 短选项等号分隔 - /// app -p:value # 短选项冒号分隔 - /// app -pvalue # 短选项直接跟值(GNU风格) - /// - /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) - /// app /parameter value # 斜杠前缀长选项 - /// app /p value # 斜杠前缀短选项 - /// app /parameter:value # 斜杠前缀冒号分隔(类MSBuild) - /// - /// # 布尔开关参数 - /// app --enable # 不带值的布尔参数(视为true) - /// app --no-feature # 否定形式(视为false) - /// app --feature=false # 显式布尔值 - /// app --feature=off # 替代布尔值形式 - /// app -e # 短格式布尔参数 - /// - /// # 位置参数和混合用法 - /// app value1 --param value2 # 位置参数和命名参数混用 - /// app --param value -- -value1 --value2 # -- 后的内容视为位置参数 - /// app -a value1 --param-b value2 /c:value3 # 混合使用不同风格 - /// - /// # 大小写不敏感(便于初学者) - /// app --PARAMETER value # 等同于 --parameter value - /// app -P value # 等同于 -p value - /// - /// # 有限支持的短选项组合 - /// app -abc # 等同于 -a -b -c(所有都是布尔开关) - /// + /// 注意:在非 Windows 系统上使用 可能在某些情况下出现解析歧义,
+ /// 这是因为 Linux/macOS 系统中,`/` 字符是合法的文件名字符,
+ /// 如果正好存在某个文件在 `/` 目录下,且文件名与某个选项名相同时,可能会被误解析为选项。 ///
Flexible, /// - /// GNU风格,支持长选项和短选项:
- /// 1. 双破折线(--) + 长选项名称,通过等号(=)或空格赋值
- /// 2. 单破折线(-) + 短选项字符,可以空格赋值,也可以紧跟参数值
- /// 3. 同时支持多个单字符选项合并(如-abc 表示 -a -b -c) + /// .NET CLI 风格。
+ /// + /// 命令和长选项采用 kebab-case 命名法,区分大小写 + /// 长选项使用 -- 前缀,如 --option-name + /// 短选项使用 - 前缀,如 -o;支持多个字符的短选项(仍是一个选项),如 -tl + /// 选项和值之间使用这些分隔符之一:冒号(:)、等号(=)、空格( ) + /// 布尔选项可以不带值,视为 true;也可以带 true/false、on/off、yes/no、1/0 等值 + /// 位置参数按顺序解析,可与选项交叉出现;使用 -- 单独一项来标记位置参数的开始,后续所有参数均视为位置参数 + /// 当值为集合时,可使用这些分隔符之一:逗号(,)、分号(;)、空格( ),也可多次指定,如 --option value1 value2 + /// 当值为字典时,使用等号(=)分隔键和值,如 --option key=value + /// ///
- /// - /// GNU风格是现代命令行工具中最广泛采用的标准之一,包括大多数Linux工具和跨平台应用程序。
- ///
- /// 详细规则:
- /// 1. 长选项以双破折线(--)开头,后跟由字母、数字、连字符组成的选项名
- /// 2. 长选项参数可以用等号(=)连接或用空格分隔
- /// 3. 短选项以单破折线(-)开头,后跟单个字符
- /// 4. 短选项参数可以直接跟在选项字符后,无需空格
- /// 5. 短选项也可以用空格分隔参数,或用等号连接参数
- /// 6. 多个不需要参数的短选项可以合并(如 -abc 等同于 -a -b -c)
- /// 7. 合并的短选项中,最后一个短选项可以带参数(如 -abc value 中 -c 接收 value 参数)
- /// 8. 双破折号(--) 作为单独参数时表示选项结束标记,之后的所有内容都被视为位置参数
- /// - /// - /// # 长选项示例 - /// app --option=value # 长选项用等号赋值 - /// app --option value # 长选项用空格赋值 - /// app --enable-feature # 布尔类型长选项(不需要值) - /// app --no-color # 否定形式的布尔长选项 - /// - /// # 短选项示例 - /// app -o=value # 短选项用等号赋值 - /// app -o value # 短选项用空格赋值 - /// app -ovalue # 短选项直接跟参数值(无空格) - /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) - /// app -abc value # 合并短选项,其中 c 接收参数值 - /// - /// # 混合使用 - /// app value1 value2 --option value -f # 位置参数 + 长选项 + 短选项 - /// app --option value -- -value1 --value2 # -- 后的 -value1 和 --value2 被视为位置参数 - /// - ///
- Gnu, + DotNet, /// - /// POSIX/UNIX风格,类似GNU但更严格:
- /// 1. 支持 - 开头的短选项,单个字符
- /// 2. 短选项可以组合使用(-abc 表示 -a -b -c)
- /// 3. 需要参数的选项必须与参数分开或使用特定格式 + /// GNU 风格。
+ /// + /// 命令和选项采用 kebab-case 命名法,区分大小写 + /// 长选项使用 -- 前缀,如 --option-name + /// 短选项使用 - 前缀,如 -o;支持多个字符的短选项组合,如 -abc(等同于 -a -b -c) + /// 选项和值之间使用这些分隔符之一:等号(=)、空格( );短选项还支持直接跟值,如 -o1.txt + /// 布尔选项可以不带值,视为 true;也可以带 true/false、on/off、yes/no、1/0 等值 + /// 位置参数按顺序解析,可与选项交叉出现;使用 -- 单独一项来标记位置参数的开始,后续所有参数均视为位置参数 + /// 当值为集合时,可使用这些分隔符之一:逗号(,)、分号(;)、空格( ),也可多次指定,如 --option value1 value2 + /// 当值为字典时,使用等号(=)分隔键和值,如 --option key=value + /// ///
- /// - /// POSIX风格是UNIX系统中规范的命令行参数格式,相比GNU风格更加严格和精简,许多传统UNIX工具遵循此规范。
- ///
- /// 详细规则:
- /// 1. 只支持短选项,以单破折线(-)开头,后跟单个字母
- /// 2. 短选项参数必须用空格与选项分隔(标准做法)
- /// 3. 不需要参数的短选项(布尔选项)可以组合在一起(如 -abc 等同于 -a -b -c)
- /// 4. 在组合的短选项中,通常不支持为最后一个选项提供参数(这点与GNU不同)
- /// 5. 标准POSIX不支持长选项(以--开头)
- /// 6. 有些遵循POSIX的工具允许用破折线后跟操作数而不是选项(如 -42 表示数字42)
- /// 7. 双破折号(--)作为选项终止符,之后的参数被当作操作数而非选项
- /// - /// - /// # 标准短选项 - /// app -o value # 短选项用空格赋值 - /// app -a # 布尔短选项 - /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) - /// - /// # 选项结束标记 - /// app -a -- -b file.txt # -- 后的 -b 被视为文件名而非选项 - /// app -a -b -- -c # -a 和 -b 是选项,-c 是参数 - /// - /// # 位置参数 - /// app file1.txt -a file2.txt # file1.txt 和 file2.txt 是位置参数 - /// - ///
- Posix, + Gnu, /// - /// .NET CLI风格,使用冒号分隔参数:
- /// 1. 短选项形式为 -参数:值
- /// 2. 长选项可以是 --参数:值
- /// 3. 也支持斜杠前缀 /参数:值(仅 Windows 环境下可用) + /// POSIX/UNIX 风格。
+ /// + /// 只支持短选项,采用单字符命名法,区分大小写 + /// 短选项使用 - 前缀,如 -o;支持多个字符的短选项组合,如 -abc(等同于 -a -b -c) + /// 选项和值之间使用空格( ) 分隔;不支持其他分隔符 + /// 布尔选项可以不带值,视为 true;也可以带 true/false、on/off、yes/no、1/0 等值 + /// 位置参数按顺序解析,可与选项交叉出现;使用 -- 单独一项来标记位置参数的开始,后续所有参数均视为位置参数 + /// 当值为集合时,可使用这些分隔符之一:逗号(,)、分号(;),也可多次指定,如 -o value1 value2 + /// 当值为字典时,使用等号(=)分隔键和值,如 -o key=value + /// ///
- /// - /// 这种风格在现代.NET工具链(dotnet CLI、NuGet、MSBuild等)和其他Microsoft工具中广泛使用。
- ///
- /// 详细规则:
- /// 1. 支持使用冒号(:)作为选项和参数值的分隔符
- /// 2. 短选项以单破折线(-)开头,后跟选项名,然后是冒号和参数值
- /// 3. 长选项以双破折线(--)开头,后跟选项名,然后是冒号和参数值
- /// 4. 也支持使用斜杠(/)作为选项前缀,仅在Windows环境中可用
- /// 5. 参数名可以是单个字母、多字符缩写或完整的单词,支持各种命名规范
- /// 6. 布尔选项通常不需要值,或使用true/false、on/off等值
- /// 7. 多个短选项一般不支持合并(与GNU/POSIX不同)
- /// 8. 某些.NET工具也接受等号(=)作为选项和值的分隔符
- /// - /// - /// # 短选项示例 - /// dotnet build -c:Release # 短选项冒号语法 - /// dotnet test -t:UnitTest # 短选项指定测试类别 - /// dotnet publish -o:./publish # 指定输出目录 - /// dotnet build -tl:off # 双字符短选项 - /// - /// # 长选项示例 - /// dotnet build --verbosity:minimal # 长选项冒号语法 - /// dotnet run --project:App1 # 指定项目 - /// msbuild --target:Rebuild # MSBuild长选项 - /// - /// # 不同命名风格 - /// dotnet build -Configuration:Release # PascalCase,单破折号 - /// dotnet build --Configuration:Release # PascalCase,双破折号 - /// dotnet build /Configuration:Release # PascalCase,斜杠前缀 - /// dotnet test --test-category:UnitTest # kebab-case,双破折号 - /// dotnet run --projectName:App1 # camelCase,双破折号 - /// - /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) - /// msbuild /p:Configuration=Release # MSBuild属性 - /// dotnet test /blame # 启用故障分析 - /// dotnet nuget push /source:feed # 指定源 - /// dotnet test /tl:off # 斜杠前缀的短选项 - /// - /// # 布尔选项 - /// dotnet build -m:1 # 最大并行度 - /// dotnet test --blame # 不带值的布尔选项 - /// dotnet build --no-restore # 否定形式的布尔选项 - /// - /// # 混合用法 - /// dotnet publish -c:Release --no-build -o:./bin - /// dotnet test -Framework:net8.0 --verbosity:normal /blame - /// - ///
- DotNet, + Posix, /// - /// PowerShell风格,使用 - 开头,但参数名称通常是完整单词或驼峰形式:
- /// 1. 长参数形式为 -参数名 值
- /// 2. 支持不带值的开关参数(开关参数)
- /// 3. 支持参数名称缩写 + /// PowerShell 风格。
+ /// + /// 命令和选项采用 PascalCase 命名法,不区分大小写 + /// 长选项使用 - 前缀,如 -OptionName + /// 短选项使用 - 前缀,如 -o;支持多个字符的短选项(仍是一个选项),如 -tl + /// 选项和值之间使用这些分隔符之一:冒号(:)、等号(=)、空格( ) + /// 布尔选项可以不带值,视为 true;也可以带 true/false、on/off、yes/no、1/0 等值 + /// 位置参数按顺序解析,可与选项交叉出现 + /// 当值为集合时,可使用这些分隔符之一:逗号(,)、分号(;)、空格( ),也可多次指定,如 -Option value1 value2 + /// 当值为字典时,使用等号(=)分隔键和值,如 -Option key=value + /// ///
/// - /// PowerShell命令行风格在微软的PowerShell脚本语言和相关工具中使用,具有独特的参数处理方式。
- ///
- /// 详细规则:
- /// 1. 参数名称前使用单个破折线(-),后跟完整的参数名(通常是Pascal或Camel大小写)
- /// 2. 参数名称与值之间用空格分隔
- /// 3. 支持参数名称的部分匹配和自动补全(只要能唯一标识参数)
- /// 4. 支持位置参数(根据位置而非参数名赋值)
- /// 5. 布尔开关参数不需要显式值(存在即为true)
- /// 6. 可以使用冒号语法传递数组或哈希表值
- /// 7. 不支持GNU/POSIX风格的短选项合并
- /// 8. 支持使用双引号或单引号包围包含空格的参数值
- /// 9. 支持参数别名(一个参数可以有多个名称)
- /// - /// - /// # 基本参数用法 - /// Get-Process -Name chrome # 带值的标准参数 - /// New-Item -Path "C:\temp" -ItemType Directory # 多个参数 - /// - /// # 开关参数(布尔参数) - /// Remove-Item -Recurse -Force # 两个开关参数(无需值) - /// Copy-Item file.txt backup/ -Verbose # 启用详细输出 - /// - /// # 参数名称缩写 - /// Get-Process -n chrome # -n 是 -Name 的缩写 - /// Get-ChildItem -Recurse -Fo *.txt # -Fo 是 -Force 的缩写(只要能唯一识别) - /// - /// # 位置参数(无需指定参数名) - /// Get-Process chrome # 位置参数,等同于 -Name chrome - /// - /// # 数组参数 - /// Get-Process -Name chrome,firefox,edge # 逗号分隔的数组 - /// Get-Process -ComputerName "srv1","srv2" # 引号包围的数组元素 - /// - /// # 复杂值和高级用法 - /// New-Object -TypeName PSObject -Property @{Name="Value"; Count=1} # 哈希表参数 - /// Invoke-Command -ScriptBlock { Get-Process } -ComputerName Server01 # 脚本块参数 - /// + /// 注意:在非 Windows 系统上使用 可能在某些情况下出现解析歧义,
+ /// 这是因为 Linux/macOS 系统中,`/` 字符是合法的文件名字符,
+ /// 如果正好存在某个文件在 `/` 目录下,且文件名与某个选项名相同时,可能会被误解析为选项。 ///
PowerShell, } diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index 0f6afda3..1b4ccf72 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -42,7 +42,7 @@ public Task RunAsync() return handler.RunAsync(); } - private (string PossibleCommandNames, ExperimentalCommandObjectCreator? Creator) MatchCreator() + private (string PossibleCommandNames, CommandObjectCreator? Creator) MatchCreator() { if (_creators.Count is 0) { @@ -75,7 +75,7 @@ public Task RunAsync() /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 /// 返回一个命令处理器构建器。 [EditorBrowsable(EditorBrowsableState.Never)] - internal CommandRunner AddHandlerCore(string? command, ExperimentalCommandObjectCreator creator, + internal CommandRunner AddHandlerCore(string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases ) { @@ -93,7 +93,7 @@ internal CommandRunner AddHandlerCore(string? command, ExperimentalCommandObject private readonly record struct CommandObjectCreationInfo { - public required ExperimentalCommandObjectCreator Creator { get; init; } + public required CommandObjectCreator Creator { get; init; } public required IReadOnlyList CommandAliases { get; init; } } diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index b1a55b5d..7db6cbdb 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils.Handlers; @@ -17,45 +16,45 @@ public static class CommandRunnerBuilderExtensions /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder) - where T : class, ICommandHandler + where T : notnull, ICommandHandler { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// @@ -66,9 +65,9 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler) - where T : class + where T : notnull { - throw MethodShouldBeInspected(); + throw CommandLine.MethodShouldBeInspected(); } /// @@ -82,10 +81,10 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class, ICommandHandler + where T : notnull, ICommandHandler { return builder.GetOrCreateRunner() .AddHandlerCore(command, creator, commandAliases); @@ -94,56 +93,56 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousCommandHandler(cl, creator, handler), commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousInt32CommandHandler(cl, creator, handler), commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousTaskCommandHandler(cl, creator, handler), commandAliases); @@ -161,10 +160,10 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler, - string? command, ExperimentalCommandObjectCreator creator, + string? command, CommandObjectCreator creator, IReadOnlyList? commandAliases = null ) - where T : class + where T : notnull { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousTaskInt32CommandHandler(cl, creator, handler), commandAliases); @@ -177,7 +176,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令处理器集合的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandlers(this ICoreCommandRunnerBuilder builder) - where T : ICommandHandlerCollection, new() + where T : notnull, ICommandHandlerCollection, new() { throw new NotImplementedException(); return builder.GetOrCreateRunner() @@ -196,10 +195,4 @@ public static IAsyncCommandRunnerBuilder AddStandardHandlers(this ICoreCommandRu { throw new NotSupportedException("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature."); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static InvalidOperationException MethodShouldBeInspected() - { - return new InvalidOperationException("源生成器本应该在编译时拦截了此方法的调用。请检查编译警告,查看 DotNetCampus.CommandLine 的源生成器是否正常工作。"); - } } diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs new file mode 100644 index 00000000..50cc3f14 --- /dev/null +++ b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs @@ -0,0 +1,104 @@ +using System.Collections; +using System.Runtime.CompilerServices; + +namespace DotNetCampus.Cli; + +/// +/// 允许用户在命令行中使用的分隔符字符集合。 +/// +#if NET8_0_OR_GREATER +[CollectionBuilder(typeof(CommandSeparatorChars), nameof(Create))] +#endif +public readonly record struct CommandSeparatorChars : IEnumerable +{ + /// + /// 一个特殊的字符(不能是 0),用来表示有分隔,但没有符。
+ /// 例如,一般的分隔符是这样:-o:1.txt;
+ /// 但有部分风格的分隔符是这样:-o1.txt。
+ /// 这时,我们需要一个特殊的字符来表示这种情况。 + ///
+ private const char Null = '\x1E'; + + /// + /// 最多支持 4 个分隔符字符。 + /// + private readonly int _chars; + + private CommandSeparatorChars(int packedChars) + { + _chars = packedChars; + } + + /// + /// 以只读列表形式返回分隔符字符集合。 + /// + /// 分隔符字符集合。 + public void CopyTo(Span buffer, out int length) + { + var chars = _chars; + length = 0; + for (var i = 0; i < 4; i++) + { + var c = (char)(chars & 0xFF); + if (c == 0) + { + break; + } + + buffer[length++] = c is Null ? (char)0 : c; + chars >>= 8; + } + } + + /// + /// 返回一个枚举器,该枚举器按添加顺序遍历 中的字符。 + /// + /// 一个可用于遍历 中字符的枚举器。 + public IEnumerator GetEnumerator() + { + var chars = _chars; + for (var i = 0; i < 4; i++) + { + var c = (char)(chars & 0xFF); + if (c == 0) + { + yield break; + } + + yield return c is Null ? (char)0 : c; + chars >>= 8; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// 从长度不大于 4 的字符(ASCII)集合创建一个新的 实例。 + /// + /// 分隔符字符集合。 + /// 新的 实例。 + /// 如果 长度大于 4。 + /// 如果 中包含 null 字符。 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CommandSeparatorChars Create(params ReadOnlySpan chars) + { + if (chars.Length > 4) + { + throw new ArgumentOutOfRangeException(nameof(chars), "最多只能指定 4 个分隔符字符。"); + } + + var packed = 0; + for (var i = chars.Length - 1; i >= 0; i--) + { + var c = chars[i]; + if (c == 0) + { + c = Null; + } + + packed = (packed << 8) | c; + } + + return new CommandSeparatorChars(packed); + } +} diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs b/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs index 33f9e130..81e510b0 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs @@ -8,4 +8,4 @@ /// /// 从已解析的命令行参数创建命令数据模型或处理器的委托。 /// -public delegate object ExperimentalCommandObjectCreator(CommandLine commandLine); +public delegate object CommandObjectCreator(CommandLine commandLine); diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs new file mode 100644 index 00000000..935c8f67 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -0,0 +1,65 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 为源生成器解析命令行提供属性赋值辅助。 +/// +/// +/// 当然,这个接口只为了给所有的实现提供实现标准。
+/// 由于所有的实现都是结构,所以不会有任何代码直接使用到这个接口。 +///
+public interface IPropertyAssignment +{ + +} + +/// +/// 专门解析来自命令行的布尔类型,并辅助赋值给属性。 +/// +public readonly struct BooleanPropertyAssignment +{ + +} + +/// +/// 专门解析来自命令行的数值类型,并辅助赋值给属性。 +/// +public readonly struct NumberPropertyAssignment +{ + +} + +/// +/// 专门解析来自命令行的字符串类型,并辅助赋值给属性。 +/// +public readonly struct StringPropertyAssignment +{ + +} + +/// +/// 专门解析来自命令行的字符串集合类型,并辅助赋值给属性。 +/// +public readonly struct StringsPropertyAssignment +{ + +} + +/// +/// 专门解析来自命令行的字典类型,并辅助赋值给属性。 +/// +public readonly struct DictionaryPropertyAssignment +{ + +} + +/// +/// 在运行时解析来自命令行的枚举类型,并辅助赋值给属性。 +/// +/// +/// 源生成器会为各个枚举生成专门的编译时类型来处理枚举的赋值。
+/// 此类型是为那些在运行时才知道枚举类型的场景准备的。 +///
+public readonly struct RuntimeEnumPropertyAssignment +{ + +} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLine.cs b/src/DotNetCampus.CommandLine/LegacyCommandLine.cs index 7100030a..e517fe83 100644 --- a/src/DotNetCampus.CommandLine/LegacyCommandLine.cs +++ b/src/DotNetCampus.CommandLine/LegacyCommandLine.cs @@ -20,7 +20,7 @@ public class LegacyCommandLine : ILegacyCoreCommandRunnerBuilder /// /// 获取解析此命令行时所使用的各种选项。 /// - internal CommandLineParsingOptions ParsingOptions { get; } + internal LegacyCommandLineParsingOptions ParsingOptions { get; } /// /// 在特定的属性不指定时,默认应使用的大小写敏感性。 @@ -116,7 +116,7 @@ private LegacyCommandLine() var options = OptionDictionary.Empty; var arguments = new ReadOnlyListRange(); CommandLineArguments = arguments; - ParsingOptions = CommandLineParsingOptions.Flexible; + ParsingOptions = LegacyCommandLineParsingOptions.Flexible; DefaultCaseSensitive = false; PossibleCommandNames = ""; MatchedUrlScheme = null; @@ -131,10 +131,10 @@ private LegacyCommandLine() PositionalArguments = arguments; } - private LegacyCommandLine(IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions = null) + private LegacyCommandLine(IReadOnlyList arguments, LegacyCommandLineParsingOptions? parsingOptions = null) { CommandLineArguments = arguments; - ParsingOptions = parsingOptions ?? CommandLineParsingOptions.Flexible; + ParsingOptions = parsingOptions ?? LegacyCommandLineParsingOptions.Flexible; DefaultCaseSensitive = parsingOptions?.CaseSensitive ?? false; (MatchedUrlScheme, var result) = CommandLineConverter.ParseCommandLineArguments(arguments, parsingOptions); PossibleCommandNames = result.PossibleCommandNames; @@ -156,7 +156,7 @@ private LegacyCommandLine(IReadOnlyList arguments, CommandLineParsingOpt /// 以此方式解析命令行参数。 /// 统一的命令行参数解析中间类型。 [Pure] - public static LegacyCommandLine Parse(IReadOnlyList args, CommandLineParsingOptions? parsingOptions = null) + public static LegacyCommandLine Parse(IReadOnlyList args, LegacyCommandLineParsingOptions? parsingOptions = null) { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(args); @@ -178,7 +178,7 @@ public static LegacyCommandLine Parse(IReadOnlyList args, CommandLinePar /// 以此方式解析命令行参数。 /// 统一的命令行参数解析中间类型。 [Pure] - public static LegacyCommandLine Parse(string singleLineCommandLineArgs, CommandLineParsingOptions? parsingOptions = null) + public static LegacyCommandLine Parse(string singleLineCommandLineArgs, LegacyCommandLineParsingOptions? parsingOptions = null) { var args = CommandLineConverter.SingleLineToList(singleLineCommandLineArgs); return new LegacyCommandLine(args, parsingOptions); diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs new file mode 100644 index 00000000..cf9e9245 --- /dev/null +++ b/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs @@ -0,0 +1,106 @@ +namespace DotNetCampus.Cli; + +/// +/// 在解析命令行参数时,指定命令行参数的解析方式。 +/// +public readonly record struct LegacyCommandLineParsingOptions() +{ + /// + public static LegacyCommandLineParsingOptions Flexible => new LegacyCommandLineParsingOptions + { + Style = LegacyCommandLineStyle.Flexible, + CaseSensitive = false, + }; + + /// + public static LegacyCommandLineParsingOptions Gnu => new LegacyCommandLineParsingOptions + { + Style = LegacyCommandLineStyle.Gnu, + CaseSensitive = true, + }; + + /// + public static LegacyCommandLineParsingOptions Posix => new LegacyCommandLineParsingOptions + { + Style = LegacyCommandLineStyle.Posix, + CaseSensitive = true, + }; + + /// + public static LegacyCommandLineParsingOptions DotNet => new LegacyCommandLineParsingOptions + { + Style = LegacyCommandLineStyle.DotNet, + CaseSensitive = false, + }; + + /// + public static LegacyCommandLineParsingOptions PowerShell => new LegacyCommandLineParsingOptions + { + Style = LegacyCommandLineStyle.PowerShell, + CaseSensitive = false, + }; + + /// + /// 以此风格解析命令行参数。 + /// + /// + /// 不指定时会自动根据用户输入的命令行参数判断风格。 + /// + public LegacyCommandLineStyle Style { get; init; } + + /// + /// 默认是大小写不敏感的,设置此值为 可以让命令行参数大小写敏感。 + /// + /// + /// 当然,可以在单独的属性上设置大小写敏感,设置后将在那个属性上覆盖此默认值。不设置的属性会使用此默认值。 + /// + public bool CaseSensitive { get; init; } + + /// + /// 此命令行解析器支持从 Web 打开本地应用时传入的参数。
+ /// 此属性指定用于 URI 协议注册的方案名(scheme name)。 + ///
+ /// + /// + /// 例如:sample://open?url=DotNetCampus%20is%20a%20great%20team
+ /// 这里的 "sample" 就是方案名。
+ /// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 + ///
+ /// /// + /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
+ ///
+ /// 详细规则:
+ /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
+ /// 2. 参数部分以问号(?)开始,后面是键值对
+ /// 3. 多个参数之间用(&)符号分隔
+ /// 4. 每个参数的键值之间用等号(=)分隔
+ /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
+ /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
+ /// 7. 支持无值参数,被视为布尔值true,如?enabled
+ /// 8. 参数值为空字符串时保留等号,如?name=
+ /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
+ /// 10. 但在某些情况下,路径的前几个部分可能会被当作命令(含子命令),例如 myapp://open/file.txt 中,open 可能是命令,file.txt 是位置参数。具体解释为位置参数还是命令取决于应用的命令行处理器实现
+ /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
+ ///
+ /// + /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) + /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 + /// myapp://user/profile?id=123&tab=info # 带层级路径 + /// sample://document/edit?id=42&mode=full # 多参数和路径组合 + /// + /// # 特殊字符与编码 + /// yourapp://search?q=hello%20world # 编码空格 + /// myapp://open?query=C%23%20programming # 特殊字符编码 + /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) + /// + /// # 无值和空值参数 + /// myapp://settings?debug # 无值参数(视为true) + /// yourapp://profile?name=&id=123 # 空字符串值 + /// + /// # 路径与命令示例 + /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 + /// myapp://open/file.txt?temporary=true # open 是命令,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 + /// + ///
+ public IReadOnlyList SchemeNames { get; init; } = []; +} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs b/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs new file mode 100644 index 00000000..3bc8fb29 --- /dev/null +++ b/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs @@ -0,0 +1,252 @@ +namespace DotNetCampus.Cli; + +/// +/// 命令行参数的风格规范。 +/// 不同的命令行工具可能使用不同的参数风格,本枚举定义了常见的几种命令行参数风格。 +/// +public enum LegacyCommandLineStyle +{ + /// + /// 灵活风格。
+ /// 根据实际传入的参数,自动识别并支持多种主流风格,包括 等风格。 + /// 适用于希望为用户提供更灵活的参数传递体验的工具。 + ///
+ /// + /// 灵活风格是一种包容性最强的命令行参数风格,旨在让不熟悉命令行操作的用户也能轻松使用。它通过智能识别尝试理解用户输入的意图,支持多种参数格式共存。
+ ///
+ /// 详细规则:
+ /// 1. 参数前缀支持多种形式:双破折线(--), 单破折线(-), 斜杠(/,仅 Windows)
+ /// 2. 参数值分隔符兼容多种形式:空格、等号(=)、冒号(:)
+ /// 3. 参数命名风格兼容kebab-case(--parameter-name)、PascalCase(-ParameterName)和camelCase
+ /// 4. 默认大小写不敏感,便于初学者使用
+ /// 5. 支持短选项(-a)和长选项(--parameter),优先识别长选项
+ /// 6. 支持布尔开关参数,可不带值或使用true/false、yes/no、on/off等常见值
+ /// 7. 支持位置参数,并可通过双破折号(--)标记位置参数的开始
+ /// 8. 支持有限的短选项组合(-abc),但当发生歧义时优先解析为单个选项
+ /// 9. 当特性之间发生冲突时,优先保留简单、直观的用法,牺牲高级但复杂的功能
+ /// 10. 自动检测并处理常见的用户错误,如选项名称拼写错误提示最接近的选项
+ /// 11. 允许不同风格在同一命令行中混合使用
+ ///
+ /// 不支持的特性(为避免冲突):
+ /// 1. 短选项组合中的最后一个选项不能直接附带参数(如-abc value,c无法接收value作为参数)
+ /// 2. 不支持POSIX风格中的特殊数字操作数形式(如-42表示数字42)
+ ///
+ /// + /// # 长选项示例(多种风格) + /// app --parameter value # GNU风格空格分隔 + /// app --parameter=value # GNU风格等号分隔 + /// app --parameter:value # DotNet风格冒号分隔 + /// app -Parameter value # PowerShell风格(Pascal命名) + /// app --param-name value # Kebab-case命名 + /// app --paramName value # CamelCase命名 + /// + /// # 短选项示例(兼容多种形式) + /// app -p value # 短选项空格分隔 + /// app -p=value # 短选项等号分隔 + /// app -p:value # 短选项冒号分隔 + /// app -pvalue # 短选项直接跟值(GNU风格) + /// + /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) + /// app /parameter value # 斜杠前缀长选项 + /// app /p value # 斜杠前缀短选项 + /// app /parameter:value # 斜杠前缀冒号分隔(类MSBuild) + /// + /// # 布尔开关参数 + /// app --enable # 不带值的布尔参数(视为true) + /// app --no-feature # 否定形式(视为false) + /// app --feature=false # 显式布尔值 + /// app --feature=off # 替代布尔值形式 + /// app -e # 短格式布尔参数 + /// + /// # 位置参数和混合用法 + /// app value1 --param value2 # 位置参数和命名参数混用 + /// app --param value -- -value1 --value2 # -- 后的内容视为位置参数 + /// app -a value1 --param-b value2 /c:value3 # 混合使用不同风格 + /// + /// # 大小写不敏感(便于初学者) + /// app --PARAMETER value # 等同于 --parameter value + /// app -P value # 等同于 -p value + /// + /// # 有限支持的短选项组合 + /// app -abc # 等同于 -a -b -c(所有都是布尔开关) + /// + ///
+ Flexible, + + /// + /// GNU风格,支持长选项和短选项:
+ /// 1. 双破折线(--) + 长选项名称,通过等号(=)或空格赋值
+ /// 2. 单破折线(-) + 短选项字符,可以空格赋值,也可以紧跟参数值
+ /// 3. 同时支持多个单字符选项合并(如-abc 表示 -a -b -c) + ///
+ /// + /// GNU风格是现代命令行工具中最广泛采用的标准之一,包括大多数Linux工具和跨平台应用程序。
+ ///
+ /// 详细规则:
+ /// 1. 长选项以双破折线(--)开头,后跟由字母、数字、连字符组成的选项名
+ /// 2. 长选项参数可以用等号(=)连接或用空格分隔
+ /// 3. 短选项以单破折线(-)开头,后跟单个字符
+ /// 4. 短选项参数可以直接跟在选项字符后,无需空格
+ /// 5. 短选项也可以用空格分隔参数,或用等号连接参数
+ /// 6. 多个不需要参数的短选项可以合并(如 -abc 等同于 -a -b -c)
+ /// 7. 合并的短选项中,最后一个短选项可以带参数(如 -abc value 中 -c 接收 value 参数)
+ /// 8. 双破折号(--) 作为单独参数时表示选项结束标记,之后的所有内容都被视为位置参数
+ /// + /// + /// # 长选项示例 + /// app --option=value # 长选项用等号赋值 + /// app --option value # 长选项用空格赋值 + /// app --enable-feature # 布尔类型长选项(不需要值) + /// app --no-color # 否定形式的布尔长选项 + /// + /// # 短选项示例 + /// app -o=value # 短选项用等号赋值 + /// app -o value # 短选项用空格赋值 + /// app -ovalue # 短选项直接跟参数值(无空格) + /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) + /// app -abc value # 合并短选项,其中 c 接收参数值 + /// + /// # 混合使用 + /// app value1 value2 --option value -f # 位置参数 + 长选项 + 短选项 + /// app --option value -- -value1 --value2 # -- 后的 -value1 和 --value2 被视为位置参数 + /// + ///
+ Gnu, + + /// + /// POSIX/UNIX风格,类似GNU但更严格:
+ /// 1. 支持 - 开头的短选项,单个字符
+ /// 2. 短选项可以组合使用(-abc 表示 -a -b -c)
+ /// 3. 需要参数的选项必须与参数分开或使用特定格式 + ///
+ /// + /// POSIX风格是UNIX系统中规范的命令行参数格式,相比GNU风格更加严格和精简,许多传统UNIX工具遵循此规范。
+ ///
+ /// 详细规则:
+ /// 1. 只支持短选项,以单破折线(-)开头,后跟单个字母
+ /// 2. 短选项参数必须用空格与选项分隔(标准做法)
+ /// 3. 不需要参数的短选项(布尔选项)可以组合在一起(如 -abc 等同于 -a -b -c)
+ /// 4. 在组合的短选项中,通常不支持为最后一个选项提供参数(这点与GNU不同)
+ /// 5. 标准POSIX不支持长选项(以--开头)
+ /// 6. 有些遵循POSIX的工具允许用破折线后跟操作数而不是选项(如 -42 表示数字42)
+ /// 7. 双破折号(--)作为选项终止符,之后的参数被当作操作数而非选项
+ /// + /// + /// # 标准短选项 + /// app -o value # 短选项用空格赋值 + /// app -a # 布尔短选项 + /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) + /// + /// # 选项结束标记 + /// app -a -- -b file.txt # -- 后的 -b 被视为文件名而非选项 + /// app -a -b -- -c # -a 和 -b 是选项,-c 是参数 + /// + /// # 位置参数 + /// app file1.txt -a file2.txt # file1.txt 和 file2.txt 是位置参数 + /// + ///
+ Posix, + + /// + /// .NET CLI风格,使用冒号分隔参数:
+ /// 1. 短选项形式为 -参数:值
+ /// 2. 长选项可以是 --参数:值
+ /// 3. 也支持斜杠前缀 /参数:值(仅 Windows 环境下可用) + ///
+ /// + /// 这种风格在现代.NET工具链(dotnet CLI、NuGet、MSBuild等)和其他Microsoft工具中广泛使用。
+ ///
+ /// 详细规则:
+ /// 1. 支持使用冒号(:)作为选项和参数值的分隔符
+ /// 2. 短选项以单破折线(-)开头,后跟选项名,然后是冒号和参数值
+ /// 3. 长选项以双破折线(--)开头,后跟选项名,然后是冒号和参数值
+ /// 4. 也支持使用斜杠(/)作为选项前缀,仅在Windows环境中可用
+ /// 5. 参数名可以是单个字母、多字符缩写或完整的单词,支持各种命名规范
+ /// 6. 布尔选项通常不需要值,或使用true/false、on/off等值
+ /// 7. 多个短选项一般不支持合并(与GNU/POSIX不同)
+ /// 8. 某些.NET工具也接受等号(=)作为选项和值的分隔符
+ /// + /// + /// # 短选项示例 + /// dotnet build -c:Release # 短选项冒号语法 + /// dotnet test -t:UnitTest # 短选项指定测试类别 + /// dotnet publish -o:./publish # 指定输出目录 + /// dotnet build -tl:off # 双字符短选项 + /// + /// # 长选项示例 + /// dotnet build --verbosity:minimal # 长选项冒号语法 + /// dotnet run --project:App1 # 指定项目 + /// msbuild --target:Rebuild # MSBuild长选项 + /// + /// # 不同命名风格 + /// dotnet build -Configuration:Release # PascalCase,单破折号 + /// dotnet build --Configuration:Release # PascalCase,双破折号 + /// dotnet build /Configuration:Release # PascalCase,斜杠前缀 + /// dotnet test --test-category:UnitTest # kebab-case,双破折号 + /// dotnet run --projectName:App1 # camelCase,双破折号 + /// + /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) + /// msbuild /p:Configuration=Release # MSBuild属性 + /// dotnet test /blame # 启用故障分析 + /// dotnet nuget push /source:feed # 指定源 + /// dotnet test /tl:off # 斜杠前缀的短选项 + /// + /// # 布尔选项 + /// dotnet build -m:1 # 最大并行度 + /// dotnet test --blame # 不带值的布尔选项 + /// dotnet build --no-restore # 否定形式的布尔选项 + /// + /// # 混合用法 + /// dotnet publish -c:Release --no-build -o:./bin + /// dotnet test -Framework:net8.0 --verbosity:normal /blame + /// + ///
+ DotNet, + + /// + /// PowerShell风格,使用 - 开头,但参数名称通常是完整单词或驼峰形式:
+ /// 1. 长参数形式为 -参数名 值
+ /// 2. 支持不带值的开关参数(开关参数)
+ /// 3. 支持参数名称缩写 + ///
+ /// + /// PowerShell命令行风格在微软的PowerShell脚本语言和相关工具中使用,具有独特的参数处理方式。
+ ///
+ /// 详细规则:
+ /// 1. 参数名称前使用单个破折线(-),后跟完整的参数名(通常是Pascal或Camel大小写)
+ /// 2. 参数名称与值之间用空格分隔
+ /// 3. 支持参数名称的部分匹配和自动补全(只要能唯一标识参数)
+ /// 4. 支持位置参数(根据位置而非参数名赋值)
+ /// 5. 布尔开关参数不需要显式值(存在即为true)
+ /// 6. 可以使用冒号语法传递数组或哈希表值
+ /// 7. 不支持GNU/POSIX风格的短选项合并
+ /// 8. 支持使用双引号或单引号包围包含空格的参数值
+ /// 9. 支持参数别名(一个参数可以有多个名称)
+ /// + /// + /// # 基本参数用法 + /// Get-Process -Name chrome # 带值的标准参数 + /// New-Item -Path "C:\temp" -ItemType Directory # 多个参数 + /// + /// # 开关参数(布尔参数) + /// Remove-Item -Recurse -Force # 两个开关参数(无需值) + /// Copy-Item file.txt backup/ -Verbose # 启用详细输出 + /// + /// # 参数名称缩写 + /// Get-Process -n chrome # -n 是 -Name 的缩写 + /// Get-ChildItem -Recurse -Fo *.txt # -Fo 是 -Force 的缩写(只要能唯一识别) + /// + /// # 位置参数(无需指定参数名) + /// Get-Process chrome # 位置参数,等同于 -Name chrome + /// + /// # 数组参数 + /// Get-Process -Name chrome,firefox,edge # 逗号分隔的数组 + /// Get-Process -ComputerName "srv1","srv2" # 引号包围的数组元素 + /// + /// # 复杂值和高级用法 + /// New-Object -TypeName PSObject -Property @{Name="Value"; Count=1} # 哈希表参数 + /// Invoke-Command -ScriptBlock { Get-Process } -ComputerName Server01 # 脚本块参数 + /// + ///
+ PowerShell, +} diff --git a/src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs b/src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs new file mode 100644 index 00000000..22048d7c --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs @@ -0,0 +1,73 @@ +namespace DotNetCampus.Cli.Utils; + +/// +/// 用节省空间的方式存储多个布尔值。 +/// +internal struct BooleanValues32() +{ + private int _value; + + internal BooleanValues32(int packedValue) : this() + { + _value = packedValue; + } + + /// + /// 获取或设置指定索引处的布尔值。 + /// + /// 索引,范围 0-31。 + internal bool this[int index] + { + get => (_value & (1 << index)) != 0; + set + { + if (value) + { + _value |= (1 << index); + } + else + { + _value &= ~(1 << index); + } + } + } + + /// + /// 获取或设置指定索引处的两个布尔值。 + /// + /// 索引,范围 0-30。 + /// 必须等于 + 1。 + internal (bool Item1, bool Item2) this[int index, int index1] + { + get + { + var bits = (_value & (3 << index)) >> index; + return ((bits & 1) != 0, (bits & 2) != 0); + } + set + { + var bits = (value.Item1 ? 1 : 0) | (value.Item2 ? 2 : 0); + _value = (_value & ~(3 << index)) | (bits << index); + } + } + + /// + /// 获取或设置指定索引处的三个布尔值。 + /// + /// 索引,范围 0-29。 + /// 必须等于 + 1。 + /// 必须等于 + 2。 + internal (bool Item1, bool Item2, bool Item3) this[int index, int index1, int index2] + { + get + { + var bits = (_value & (7 << index)) >> index; + return ((bits & 1) != 0, (bits & 2) != 0, (bits & 4) != 0); + } + set + { + var bits = (value.Item1 ? 1 : 0) | (value.Item2 ? 2 : 0) | (value.Item3 ? 4 : 0); + _value = (_value & ~(7 << index)) | (bits << index); + } + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 97afc1b7..116e3707 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -66,7 +66,7 @@ internal static IReadOnlyList SingleLineToList(string singleLineCommandL } public static (string? MatchedUrlScheme, CommandLineParsedResult Result) ParseCommandLineArguments( - IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions) + IReadOnlyList arguments, LegacyCommandLineParsingOptions? parsingOptions) { var matchedUrlScheme = arguments.Count is 1 && parsingOptions?.SchemeNames is { Count: > 0 } schemeNames ? schemeNames.FirstOrDefault(x => arguments[0].StartsWith($"{x}://", StringComparison.OrdinalIgnoreCase)) @@ -75,11 +75,11 @@ public static (string? MatchedUrlScheme, CommandLineParsedResult Result) ParseCo ICommandLineParser parser = (matchUrlScheme: matchedUrlScheme, parsingOptions?.Style) switch { ({ } scheme, _) => new UrlStyleParser(scheme), - (_, CommandLineStyle.Flexible) => new FlexibleStyleParser(), - (_, CommandLineStyle.Gnu) => new GnuStyleParser(), - (_, CommandLineStyle.Posix) => new PosixStyleParser(), - (_, CommandLineStyle.DotNet) => new DotNetStyleParser(), - (_, CommandLineStyle.PowerShell) => new PowerShellStyleParser(), + (_, LegacyCommandLineStyle.Flexible) => new FlexibleStyleParser(), + (_, LegacyCommandLineStyle.Gnu) => new GnuStyleParser(), + (_, LegacyCommandLineStyle.Posix) => new PosixStyleParser(), + (_, LegacyCommandLineStyle.DotNet) => new DotNetStyleParser(), + (_, LegacyCommandLineStyle.PowerShell) => new PowerShellStyleParser(), _ => new FlexibleStyleParser(), }; return (matchedUrlScheme, parser.Parse(arguments)); diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index f8441141..0c2a682e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -22,9 +22,9 @@ public Task RunAsync() internal sealed class AnonymousCommandHandler( CommandLine commandLine, - ExperimentalCommandObjectCreator creator, + CommandObjectCreator creator, Action handler) : ICommandHandler - where T : class + where T : notnull { private T? _options; @@ -42,9 +42,9 @@ public Task RunAsync() internal sealed class AnonymousInt32CommandHandler( CommandLine commandLine, - ExperimentalCommandObjectCreator creator, + CommandObjectCreator creator, Func handler) : ICommandHandler - where T : class + where T : notnull { private T? _options; @@ -62,9 +62,9 @@ public Task RunAsync() internal sealed class AnonymousTaskCommandHandler( CommandLine commandLine, - ExperimentalCommandObjectCreator creator, + CommandObjectCreator creator, Func handler) : ICommandHandler - where T : class + where T : notnull { private T? _options; @@ -82,9 +82,9 @@ public async Task RunAsync() internal sealed class AnonymousTaskInt32CommandHandler( CommandLine commandLine, - ExperimentalCommandObjectCreator creator, + CommandObjectCreator creator, Func> handler) : ICommandHandler - where T : class + where T : notnull { private T? _options; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs index b3f93141..909f8416 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs @@ -5,7 +5,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// +/// internal sealed class DotNetStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs index c3301428..351d2a89 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs @@ -4,7 +4,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// +/// internal sealed class FlexibleStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs new file mode 100644 index 00000000..15beb4c0 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs @@ -0,0 +1,91 @@ +using DotNetCampus.Cli.Utils.Collections; + +namespace DotNetCampus.Cli.Utils.Parsers; + +public class GenericStyleParser +{ + public void Parse(ReadOnlySpan arguments) + { + OptionName? lastOption = null; + var lastType = CommandArgumentType.Start; + + for (var i = 0; i < arguments.Length; i++) + { + var argument = arguments[i]; + var result = CommandArgumentType.Parse(argument, lastType); + + } + } +} + +internal readonly ref struct CommandArgumentParser(CommandArgumentType type) +{ + public CommandArgumentType Type { get; } = type; + public OptionName Option { get; private init; } + public ReadOnlySpan Value { get; private init; } + + public static CommandArgumentParser Parse(string argument, CommandArgumentType lastType) + { + + } +} + + + +internal enum CommandArgumentType +{ + /// + /// 尚未开始解析。 + /// + Start, + + /// + /// 命令(主命令、子命令或多级子命令)。 + /// + Command, + + /// + /// 混在选项间的位置参数。 + /// + PositionalArgument, + + /// + /// 长选项。--option -Option /option -tl /tl + /// + LongOption, + + /// + /// 带值的长选项。--option:value -Option:value /option:value -tl:off /tl:off + /// + LongOptionWithValue, + + /// + /// 短选项。-o /o + /// + ShortOption, + + /// + /// 带值的短选项。-o:value /o:value + /// + ShortOptionWithValue, + + /// + /// 多个短选项。-abc + /// + MultiShortOptions, + + /// + /// 选项值。value + /// + OptionValue, + + /// + /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 + /// + PositionalArgumentSeparator, + + /// + /// 后置的位置参数。 + /// + PostPositionalArgument, +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs index ebbb11c6..c34ab634 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs @@ -3,7 +3,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// +/// internal sealed class GnuStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = false; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs index c461fa5b..351fc145 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs @@ -3,7 +3,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// +/// internal sealed class PosixStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = false; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs index 86a0c18a..c2e06861 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs @@ -2,7 +2,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// +/// internal sealed class PowerShellStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; From 654b937c4e291bb8da54ab5bbac5201817d812d3 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 13 Sep 2025 17:51:48 +0800 Subject: [PATCH 003/193] =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E4=B8=80=E4=B8=8B?= =?UTF-8?q?=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 6 +-- .../CommandSeparatorChars.cs | 44 +++++++++++-------- ...{BooleanValues32.cs => BooleanValues16.cs} | 20 ++++----- 3 files changed, 38 insertions(+), 32 deletions(-) rename src/DotNetCampus.CommandLine/Utils/{BooleanValues32.cs => BooleanValues16.cs} (74%) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 46728d78..8ea40edf 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -98,15 +98,15 @@ public readonly record struct CommandLineParsingOptions ///
public readonly record struct CommandLineStyleDetails() { - private readonly BooleanValues32 _booleans; + private readonly BooleanValues16 _booleans; /// /// 直接由程序员提前算好各种属性赋值完成后的魔数,节省应用程序启动期间的额外计算。 /// /// 魔数。 - internal CommandLineStyleDetails(int magic) : this() + internal CommandLineStyleDetails(ushort magic) : this() { - _booleans = new BooleanValues32(magic); + _booleans = new BooleanValues16(magic); } /// diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs index 50cc3f14..d83a8783 100644 --- a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs @@ -4,7 +4,8 @@ namespace DotNetCampus.Cli; /// -/// 允许用户在命令行中使用的分隔符字符集合。 +/// 允许用户在命令行中使用的分隔符字符集合。
+/// 用节省空间的方式存储不小于长度 4 的多个字符。 ///
#if NET8_0_OR_GREATER [CollectionBuilder(typeof(CommandSeparatorChars), nameof(Create))] @@ -22,9 +23,9 @@ namespace DotNetCampus.Cli; /// /// 最多支持 4 个分隔符字符。 /// - private readonly int _chars; + private readonly ushort _chars; - private CommandSeparatorChars(int packedChars) + private CommandSeparatorChars(ushort packedChars) { _chars = packedChars; } @@ -35,18 +36,23 @@ private CommandSeparatorChars(int packedChars) /// 分隔符字符集合。 public void CopyTo(Span buffer, out int length) { - var chars = _chars; length = 0; - for (var i = 0; i < 4; i++) + var packed = _chars; + while (packed != 0) { - var c = (char)(chars & 0xFF); - if (c == 0) + var c = (char)(packed & 0xF); + if (c == Null) { - break; + c = '\0'; } - buffer[length++] = c is Null ? (char)0 : c; - chars >>= 8; + if (length < buffer.Length) + { + buffer[length] = c; + } + + length++; + packed >>= 4; } } @@ -56,17 +62,17 @@ public void CopyTo(Span buffer, out int length) /// 一个可用于遍历 中字符的枚举器。 public IEnumerator GetEnumerator() { - var chars = _chars; - for (var i = 0; i < 4; i++) + var packed = _chars; + while (packed != 0) { - var c = (char)(chars & 0xFF); - if (c == 0) + var c = (char)(packed & 0xF); + if (c == Null) { - yield break; + c = '\0'; } - yield return c is Null ? (char)0 : c; - chars >>= 8; + yield return c; + packed >>= 4; } } @@ -87,7 +93,7 @@ public static CommandSeparatorChars Create(params ReadOnlySpan chars) throw new ArgumentOutOfRangeException(nameof(chars), "最多只能指定 4 个分隔符字符。"); } - var packed = 0; + ushort packed = 0; for (var i = chars.Length - 1; i >= 0; i--) { var c = chars[i]; @@ -96,7 +102,7 @@ public static CommandSeparatorChars Create(params ReadOnlySpan chars) c = Null; } - packed = (packed << 8) | c; + packed = (ushort)((packed << 4) | c); } return new CommandSeparatorChars(packed); diff --git a/src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs b/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs similarity index 74% rename from src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs rename to src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs index 22048d7c..ae93a34e 100644 --- a/src/DotNetCampus.CommandLine/Utils/BooleanValues32.cs +++ b/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs @@ -3,11 +3,11 @@ namespace DotNetCampus.Cli.Utils; /// /// 用节省空间的方式存储多个布尔值。 /// -internal struct BooleanValues32() +internal struct BooleanValues16() { - private int _value; + private ushort _value; - internal BooleanValues32(int packedValue) : this() + internal BooleanValues16(ushort packedValue) : this() { _value = packedValue; } @@ -15,7 +15,7 @@ internal BooleanValues32(int packedValue) : this() /// /// 获取或设置指定索引处的布尔值。 /// - /// 索引,范围 0-31。 + /// 索引,范围 0-15。 internal bool this[int index] { get => (_value & (1 << index)) != 0; @@ -23,11 +23,11 @@ internal bool this[int index] { if (value) { - _value |= (1 << index); + _value |= (ushort)(1 << index); } else { - _value &= ~(1 << index); + _value &= (ushort)~(1 << index); } } } @@ -35,7 +35,7 @@ internal bool this[int index] /// /// 获取或设置指定索引处的两个布尔值。 /// - /// 索引,范围 0-30。 + /// 索引,范围 0-14。 /// 必须等于 + 1。 internal (bool Item1, bool Item2) this[int index, int index1] { @@ -47,14 +47,14 @@ internal bool this[int index] set { var bits = (value.Item1 ? 1 : 0) | (value.Item2 ? 2 : 0); - _value = (_value & ~(3 << index)) | (bits << index); + _value = (ushort)((_value & ~(3 << index)) | (bits << index)); } } /// /// 获取或设置指定索引处的三个布尔值。 /// - /// 索引,范围 0-29。 + /// 索引,范围 0-13。 /// 必须等于 + 1。 /// 必须等于 + 2。 internal (bool Item1, bool Item2, bool Item3) this[int index, int index1, int index2] @@ -67,7 +67,7 @@ internal bool this[int index] set { var bits = (value.Item1 ? 1 : 0) | (value.Item2 ? 2 : 0) | (value.Item3 ? 4 : 0); - _value = (_value & ~(7 << index)) | (bits << index); + _value = (ushort)((_value & ~(7 << index)) | (bits << index)); } } } From 958f40cfea3aa118dc1109f2894a3241963283c7 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 14 Sep 2025 11:28:04 +0800 Subject: [PATCH 004/193] =?UTF-8?q?=E7=BC=96=E5=86=99=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E7=9A=84=E8=A7=A3=E6=9E=90=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandLine.cs | 12 + .../CommandLineParsingOptions.cs | 77 ++- .../CommandSeparatorChars.cs | 20 +- .../Utils/Collections/OptionDictionary.cs | 22 +- .../Utils/CommandLineConverter.cs | 2 +- .../Utils/Parsers/Callbacks.cs | 26 + ...cStyleParser.cs => CommandArgumentType.cs} | 50 +- .../Utils/Parsers/CommandLineParser.cs | 446 ++++++++++++++++++ .../Utils/Parsers/CommandLineParsingResult.cs | 28 ++ .../Utils/Parsers/DotNetStyleParser.cs | 22 +- .../Utils/Parsers/FlexibleStyleParser.cs | 22 +- .../Utils/Parsers/GnuStyleParser.cs | 24 +- .../Utils/Parsers/ICommandLineParser.cs | 2 +- ...lt.cs => LegacyCommandLineParsedResult.cs} | 2 +- .../Utils/Parsers/OptionName.cs | 19 + .../Utils/Parsers/PosixStyleParser.cs | 16 +- .../Utils/Parsers/PowerShellStyleParser.cs | 14 +- .../Utils/Parsers/UrlStyleParser.cs | 12 +- 18 files changed, 691 insertions(+), 125 deletions(-) create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs rename src/DotNetCampus.CommandLine/Utils/Parsers/{GenericStyleParser.cs => CommandArgumentType.cs} (61%) create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs rename src/DotNetCampus.CommandLine/Utils/Parsers/{CommandLineParsedResult.cs => LegacyCommandLineParsedResult.cs} (92%) create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index f614b462..0deef366 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -79,7 +79,9 @@ public static CommandLine Parse(string singleLineCommandLineArgs, CommandLinePar /// 要转换的类型。 /// 转换后的实例。 [Pure] +#pragma warning disable CA1822 public T As() where T : notnull => throw MethodShouldBeInspected(); +#pragma warning restore CA1822 /// /// 尝试将命令行参数转换为指定类型的实例。 @@ -93,6 +95,16 @@ public T As(CommandObjectCreator creator) where T : notnull return (T)creator(this); } + /// + /// 输出传入的命令行参数字符串。 + /// + /// 传入的命令行参数字符串。 + [Pure] + public override string ToString() + { + return string.Join(" ", CommandLineArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); + } + CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); /// diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 8ea40edf..50d52b42 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -15,8 +15,10 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = false, SupportsLongOption = true, SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.Both, - OptionPrefix = CommandOptionPrefix.DoubleDash, + OptionPrefix = CommandOptionPrefix.Any, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), DictionaryValueSeparators = CommandSeparatorChars.Create('='), @@ -31,6 +33,8 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = true, SupportsLongOption = true, SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), @@ -47,6 +51,8 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = true, SupportsLongOption = true, SupportsShortOption = true, + SupportsShortOptionCombination = true, + SupportsShortOptionValueWithoutSeparator = true, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create('=', ' '), @@ -63,6 +69,8 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = true, SupportsLongOption = false, SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.CamelCase, OptionPrefix = CommandOptionPrefix.SingleDash, OptionValueSeparators = CommandSeparatorChars.Create(' '), @@ -79,6 +87,8 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = false, SupportsLongOption = true, SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.PascalCase, OptionPrefix = CommandOptionPrefix.Slash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), @@ -188,6 +198,29 @@ public bool SupportsShortOption init => _booleans[7] = value; } + /// + /// 当支持短选项时,是否支持将多个短选项组合在一起使用(短选项捆绑)。
+ /// 例如 -abc 等同于 -a -b -c。
+ /// 如果为 ,则 -abc 会被视为一个名为 "abc" 的短选项。 + ///
+ public bool SupportsShortOptionCombination + { + get => _booleans[8]; + init => _booleans[8] = value; + } + + /// + /// 当支持短选项时,是否支持短选项直接跟值(不使用分隔符)。
+ /// 例如 -abc 会被视为短选项 -a,值为 "bc"。
+ /// 如果为 ,则会根据 的值来决定 + /// -abc 是一个名为 "abc" 的短选项,还是 -a -b -c 三个短选项。 + ///
+ public bool SupportsShortOptionValueWithoutSeparator + { + get => _booleans[9]; + init => _booleans[9] = value; + } + /// /// 允许用户使用哪些分隔符来分隔选项名和选项值。
/// 如 ':', '=', ' ' 分别对应: --option:value, --option=value, --option value。 @@ -220,7 +253,7 @@ public bool SupportsShortOption /// /// 虽然在不区分大小写时, 看起来是一样的,但在输出帮助文档时会以设定的为准。 /// -public enum CommandNamingPolicy +public enum CommandNamingPolicy : byte { /// /// PascalCase 风格命名。 @@ -256,7 +289,7 @@ public enum CommandNamingPolicy /// /// 指定命令行选项前缀的风格。 /// -public enum CommandOptionPrefix +public enum CommandOptionPrefix : byte { /// /// 使用双短横线(--)作为长选项前缀,使用单个短横线(-)作为短选项前缀。 @@ -264,12 +297,46 @@ public enum CommandOptionPrefix DoubleDash, /// - /// 使用单个短横线(-)作为长选项和短选项前缀。 + /// 使用单个短横线(-)作为长选项和短选项前缀。
+ /// 注意:如果启用此选项,将不支持短选项组合和短选项直接跟值;仍支持多字符短选项,但解析会造成轻微的性能下降(因为会两次尝试匹配选项名)。 ///
SingleDash, /// - /// 使用斜杠(/)作为长选项和短选项前缀。 + /// 使用斜杠(/)作为长选项和短选项前缀。
+ /// 注意:如果启用此选项,将不支持短选项组合和短选项直接跟值;仍支持多字符短选项,但解析会造成轻微的性能下降(因为会两次尝试匹配选项名)。 ///
Slash, + + /// + /// 允许使用任意一种前缀风格(-、--、/)。
+ /// 注意:如果启用此选项,将不支持短选项组合和短选项直接跟值;仍支持多字符短选项,但解析会造成轻微的性能下降(因为会两次尝试匹配选项名)。 + ///
+ Any, +} + +/// +/// 选项值的类型。此枚举中的选项值类型会影响到选项值的解析方式。 +/// +public enum OptionValueType : byte +{ + /// + /// 普通值。只解析一个参数。 + /// + Normal, + + /// + /// 布尔值。会尝试解析一个参数,如果无法解析,则视为 。 + /// + Boolean, + + /// + /// 集合值。会尝试解析多个参数,直到遇到下一个选项或位置参数分隔符为止。 + /// + Collection, + + /// + /// 用户输入的选项没有命中到任何已知的选项。 + /// + NotExist, } diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs index d83a8783..f586171c 100644 --- a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs @@ -12,14 +12,6 @@ namespace DotNetCampus.Cli; #endif public readonly record struct CommandSeparatorChars : IEnumerable { - /// - /// 一个特殊的字符(不能是 0),用来表示有分隔,但没有符。
- /// 例如,一般的分隔符是这样:-o:1.txt;
- /// 但有部分风格的分隔符是这样:-o1.txt。
- /// 这时,我们需要一个特殊的字符来表示这种情况。 - ///
- private const char Null = '\x1E'; - /// /// 最多支持 4 个分隔符字符。 /// @@ -41,11 +33,6 @@ public void CopyTo(Span buffer, out int length) while (packed != 0) { var c = (char)(packed & 0xF); - if (c == Null) - { - c = '\0'; - } - if (length < buffer.Length) { buffer[length] = c; @@ -66,11 +53,6 @@ public IEnumerator GetEnumerator() while (packed != 0) { var c = (char)(packed & 0xF); - if (c == Null) - { - c = '\0'; - } - yield return c; packed >>= 4; } @@ -99,7 +81,7 @@ public static CommandSeparatorChars Create(params ReadOnlySpan chars) var c = chars[i]; if (c == 0) { - c = Null; + throw new ArgumentException("不支持 null 字符作为分隔符。", nameof(chars)); } packed = (ushort)((packed << 4) | c); diff --git a/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs b/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs index 33c3f0b6..9bf2aba6 100644 --- a/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs +++ b/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs @@ -93,7 +93,7 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out IReadOnlyList string.Equals(p.Key, optionNameText, _stringComparer)); @@ -103,7 +103,7 @@ public void AddOption(OptionName optionName) } } - public void AddValue(OptionName optionName, string value) + public void AddValue(LegacyOptionName optionName, string value) { var optionNameText = optionName.ToString(); var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); @@ -117,7 +117,7 @@ public void AddValue(OptionName optionName, string value) } } - public void AddValues(OptionName optionName, IReadOnlyList values) + public void AddValues(LegacyOptionName optionName, IReadOnlyList values) { var optionNameText = optionName.ToString(); var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); @@ -131,7 +131,7 @@ public void AddValues(OptionName optionName, IReadOnlyList values) } } - public void UpdateValue(OptionName optionName, string value) + public void UpdateValue(LegacyOptionName optionName, string value) { var optionNameText = optionName.ToString(); var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); @@ -172,7 +172,7 @@ public IEnumerator>> GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } -internal readonly record struct OptionName(string Argument, Range Range) : IEnumerable +internal readonly record struct LegacyOptionName(string Argument, Range Range) : IEnumerable { public char this[int index] { @@ -198,7 +198,7 @@ public ReadOnlySpan AsSpan() } #endif - public bool Equals(OptionName? other) + public bool Equals(LegacyOptionName? other) { if (other is null) { @@ -223,7 +223,7 @@ public bool Equals(OptionName? other) return true; } - public bool Equals(OptionName other, bool caseSensitive) + public bool Equals(LegacyOptionName other, bool caseSensitive) { var (thisOffset, thisLength) = Range.GetOffsetAndLength(Argument.Length); var (thatOffset, thatLength) = other.Range.GetOffsetAndLength(other.Argument.Length); @@ -265,9 +265,9 @@ public IEnumerator GetEnumerator() public override string ToString() => AsSpan().ToString(); - public static implicit operator OptionName(string optionName) => new OptionName(optionName, Range.All); + public static implicit operator LegacyOptionName(string optionName) => new LegacyOptionName(optionName, Range.All); - public static implicit operator OptionName(char optionName) => new OptionName(optionName.ToString(), Range.All); + public static implicit operator LegacyOptionName(char optionName) => new LegacyOptionName(optionName.ToString(), Range.All); public static bool IsValidOptionName(ReadOnlySpan span) { @@ -290,10 +290,10 @@ public static bool IsValidOptionName(ReadOnlySpan span) return true; } - public static OptionName MakeKebabCase(ReadOnlySpan span, bool isUpperSeparator) + public static LegacyOptionName MakeKebabCase(ReadOnlySpan span, bool isUpperSeparator) { var name = NamingHelper.MakeKebabCase(span.ToString(), isUpperSeparator, false); - return new OptionName(name, Range.All); + return new LegacyOptionName(name, Range.All); } public static string MakeKebabCase(ReadOnlySpan span) diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 116e3707..58d143be 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -65,7 +65,7 @@ internal static IReadOnlyList SingleLineToList(string singleLineCommandL return [..parts.Select(part => singleLineCommandLineArgs[part])]; } - public static (string? MatchedUrlScheme, CommandLineParsedResult Result) ParseCommandLineArguments( + public static (string? MatchedUrlScheme, LegacyCommandLineParsedResult Result) ParseCommandLineArguments( IReadOnlyList arguments, LegacyCommandLineParsingOptions? parsingOptions) { var matchedUrlScheme = arguments.Count is 1 && parsingOptions?.SchemeNames is { Count: > 0 } schemeNames diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs new file mode 100644 index 00000000..b1381775 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs @@ -0,0 +1,26 @@ +namespace DotNetCampus.Cli.Utils.Parsers; + +/// +/// 要求源生成器判断某个索引处的参数是否为命令(主命令、子命令或多级子命令)。 +/// +/// 要判断的参数的索引。 +/// 如果此参数为命令(主命令、子命令或多级子命令),则返回 ;否则返回 +public delegate bool CheckIsCommandCallback(int argumentIndex); + +/// +/// 要求源生成器匹配长名称,返回此长选项的值类型。 +/// +/// 由用户输入的长名称(已去掉前缀符号和后续所带的值,未处理命名法变换)。 +/// 如果此参数未指定大小写敏感性,则使用此默认值。 +/// 由开发者配置的允许的命名法。 +/// 此长选项的值类型。 +public delegate OptionValueType LongOptionMatchingCallback(ReadOnlySpan longOption, bool defaultCaseSensitive, + params ReadOnlySpan allowedNamingPolicies); + +/// +/// 要求源生成器匹配短名称,返回此短选项的值类型。 +/// +/// 由用户输入的短名称(已去掉前缀符号和后续所带的值,包含多个字符时也只允许匹配一个短选项)。 +/// 如果此参数未指定大小写敏感性,则使用此默认值。 +/// 此短选项的值类型。 +public delegate OptionValueType ShortOptionMatchingCallback(ReadOnlySpan shortOption, bool defaultCaseSensitive); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs similarity index 61% rename from src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs rename to src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs index 15beb4c0..470ed7f7 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/GenericStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs @@ -1,37 +1,8 @@ -using DotNetCampus.Cli.Utils.Collections; - namespace DotNetCampus.Cli.Utils.Parsers; -public class GenericStyleParser -{ - public void Parse(ReadOnlySpan arguments) - { - OptionName? lastOption = null; - var lastType = CommandArgumentType.Start; - - for (var i = 0; i < arguments.Length; i++) - { - var argument = arguments[i]; - var result = CommandArgumentType.Parse(argument, lastType); - - } - } -} - -internal readonly ref struct CommandArgumentParser(CommandArgumentType type) -{ - public CommandArgumentType Type { get; } = type; - public OptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static CommandArgumentParser Parse(string argument, CommandArgumentType lastType) - { - - } -} - - - +/// +/// 命令行参数的类型。 +/// internal enum CommandArgumentType { /// @@ -69,11 +40,26 @@ internal enum CommandArgumentType /// ShortOptionWithValue, + /// + /// 无法确定长还是短的选项。-o /o /option -tl /tl -Option + /// + Option, + + /// + /// 带值的无法确定长还是短的选项。-o:value /o:value /option:value -tl:off /tl:off -Option:value + /// + OptionWithValue, + /// /// 多个短选项。-abc /// MultiShortOptions, + /// + /// 不确定是多个短选项,还是一个无分隔符的带值短选项。-a1.txt + /// + MultiShortOptionsOrShortOptionConcatWithValue, + /// /// 选项值。value /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs new file mode 100644 index 00000000..76e8be32 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -0,0 +1,446 @@ +using Cat = DotNetCampus.Cli.Utils.Parsers.CommandArgumentType; + +namespace DotNetCampus.Cli.Utils.Parsers; + +/// +/// 通用的命令行参数解析器。(此解析器不可解析 URL 类型的参数。) +/// +public readonly ref struct CommandLineParser +{ + private readonly CommandLine _commandLine; + private readonly string _commandObjectName; + + /// + /// 通用的命令行参数解析器。(此解析器不可解析 URL 类型的参数。) + /// + /// 要解析的命令行参数。 + /// 正在解析此参数的命令对象的名称。 + public CommandLineParser(CommandLine commandLine, string commandObjectName) + { + _commandLine = commandLine; + _commandObjectName = commandObjectName; + Style = commandLine.ParsingOptions.Style; + NamingPolicy = Style.NamingPolicy; + OptionPrefix = Style.OptionPrefix; + CaseSensitive = Style.CaseSensitive; + SupportsLongOption = Style.SupportsLongOption; + SupportsShortOption = Style.SupportsShortOption; + SupportsShortOptionCombination = Style.SupportsShortOptionCombination; + SupportsShortOptionValueWithoutSeparator = Style.SupportsShortOptionValueWithoutSeparator; + } + + internal CommandLineStyleDetails Style { get; } + + internal CommandNamingPolicy NamingPolicy { get; } + + internal CommandOptionPrefix OptionPrefix { get; } + + internal bool CaseSensitive { get; } + + internal bool SupportsLongOption { get; } + + internal bool SupportsShortOption { get; } + + internal bool SupportsShortOptionCombination { get; } + + internal bool SupportsShortOptionValueWithoutSeparator { get; } + + /// + /// 要求源生成器判断某个索引处的参数是否为命令(主命令、子命令或多级子命令)。 + /// + public required CheckIsCommandCallback IsCommand { get; init; } + + /// + /// 要求源生成器匹配长名称,返回此长选项的值类型。 + /// + public required LongOptionMatchingCallback MatchLongOption { get; init; } + + /// + /// 要求源生成器匹配短名称,返回此短选项的值类型。 + /// + public required ShortOptionMatchingCallback MatchShortOption { get; init; } + + public CommandLineParsingResult Parse() + { + var arguments = _commandLine.CommandLineArguments; + var lastOptionName = new OptionName(false, []); + var lastOptionType = OptionValueType.Normal; + var lastType = Cat.Start; + + for (var index = 0; index < arguments.Count; index++) + { + // 跳过命令(主命令、子命令、多级子命令)。 + var argument = arguments[index]; + if (IsCommand(index)) + { + continue; + } + + // 解析当前参数。 + var part = new CommandArgumentPart(this, index, argument, lastType, lastOptionType); + part.Parse(); + var (currentType, optionName, value) = part; + + // 更新状态。 + lastType = currentType; + if (currentType is Cat.LongOption or Cat.ShortOption or Cat.Option) + { + // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 + lastOptionName = optionName; + var optionType = currentType switch + { + Cat.LongOption => MatchLongOption(optionName.Name, CaseSensitive, NamingPolicy), + Cat.ShortOption => MatchShortOption(optionName.Name, CaseSensitive), + _ => MatchLongOption(optionName.Name, CaseSensitive, NamingPolicy) switch + { + OptionValueType.NotExist => MatchShortOption(optionName.Name, CaseSensitive), + var t => t, + }, + }; + if (optionType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + } + lastOptionType = optionType; + } + else if (currentType is Cat.OptionValue && lastOptionType is OptionValueType.Collection) + { + // 如果当前是选项值,且上个选项是一个集合值,则继续使用上个选项。 + } + else + { + // 其他情况,都需要清空上一个选项,避免误用。 + lastOptionName = new OptionName(false, []); + lastOptionType = OptionValueType.Normal; + } + + // 处理解析结果。 + } + } +} + +/// +/// 辅助解析命令行参数中的其中一个参数。 +/// +internal ref struct CommandArgumentPart +{ + private readonly CommandLineParser _parser; + private readonly int _index; + private readonly string _argument; + private readonly Cat _lastType; + private readonly OptionValueType _lastOptionType; + + /// + /// 辅助解析命令行参数中的其中一个参数。 + /// + /// 正在使用的命令行参数解析器。 + /// 正在解析的参数的索引。 + /// 要解析的参数。 + /// 上一个参数的类型,初始为 。 + /// 上一个参数的选项值类型,如果上一个参数不是选项,则为默认值。 + public CommandArgumentPart(CommandLineParser parser, int index, string argument, Cat lastType, OptionValueType lastOptionType) + { + _parser = parser; + _index = index; + _argument = argument; + _lastType = lastType; + _lastOptionType = lastOptionType; + } + + /// + /// 解析完成后,发现此参数的类型。 + /// + public Cat Type { get; private set; } + + /// + /// 如果此参数是一个选项(长选项或短选项),则为此选项的名称;否则为默认值。 + /// + public OptionName Option { get; private set; } + + /// + /// 如果此参数包含值(位置参数或选项值),则为此值;否则为默认值。 + /// + public ReadOnlySpan Value { get; private set; } + + /// + /// 将此参数解构为各个部分。 + /// + /// 此参数的类型。 + /// 如果此参数是一个选项(长选项或短选项),则为此选项的名称;否则为默认值。 + /// 如果此参数包含值(位置参数或选项值),则为此值;否则为默认值。 + public void Deconstruct(out Cat type, out OptionName optionName, out ReadOnlySpan value) + { + type = Type; + optionName = Option; + value = Value; + } + + /// + /// 开始解析这个参数,可通过解构获得解析结果。 + /// + /// + /// 返回值没有意义,纯粹为了使用 switch 表达式。 + /// + public bool Parse() => _lastType switch + { + Cat.Start or Cat.Command => ParseCommandRegion(), + Cat.PositionalArgumentSeparator or Cat.PostPositionalArgument => ParsePostPositionalArgumentRegion(), + _ => ParseOptionAndPositionalArgumentRegion(), + }; + + /// + /// 起点/命令/子命令区 --> 命令/子命令区 + /// 起点/命令/子命令区 --> 选项和位置参数混合区 + /// + private bool ParseCommandRegion() + { + // 由于命令已提前跳过,所以这里直接进入选项和位置参数混合区。 + return ParseOptionAndPositionalArgumentRegion(); + } + + /// + /// 选项和位置参数混合区 --> 后置位置参数区 + /// 选项和位置参数混合区 --> 选项和位置参数混合区 + /// + private bool ParseOptionAndPositionalArgumentRegion() + { + var isPostPositionalArgument = string.Equals(_argument, "--", StringComparison.Ordinal); + if (isPostPositionalArgument) + { + Type = Cat.PositionalArgumentSeparator; + return true; + } + + return _lastType switch + { + // 值已经被上一个选项消费掉了,必须是新的选项或位置参数。 + Cat.PositionalArgument or Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue => ParseOptionOrPositionalArgument(), + // 多个短选项,后面不允许带值。 + Cat.MultiShortOptions => ParseOptionOrPositionalArgument(), + // 上一个是选项: + Cat.LongOption or Cat.ShortOption or Cat.Option => (_lastOptionType switch + { + // 如果是布尔选项,则后面只能跟布尔值,否则只能是新的选项或位置参数。 + OptionValueType.Boolean => ParseBooleanOptionValueOrNewOptionOrPositionalArgument(), + // 如果是集合选项,则后面可以跟多个值,直到遇到新的选项或位置参数分隔符为止。 + OptionValueType.Collection => ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), + // 如果是普通选项,则后面只能是选项值。 + _ => ParseOptionValue(_argument.AsSpan()), + }), + _ => throw new InvalidOperationException($"解析上一个参数时已进入错误的状态:{_lastType}。"), + }; + } + + /// + /// 后置位置参数区 --> 后置位置参数区 + /// + private bool ParsePostPositionalArgumentRegion() + { + Type = Cat.PostPositionalArgument; + Value = _argument.AsSpan(); + return true; + } + + /// + /// 选项和位置参数混合区(状态内部) + /// 起点 --> 位置参数 + /// 起点 --> 选项 + /// + /// + private bool ParseOptionOrPositionalArgument() + { + var argument = _argument.AsSpan(); + if (argument.Length is 0 or 1) + { + // 空参数或单个字符(无法组成选项),视为位置参数。 + Type = Cat.PositionalArgument; + Value = argument; + return true; + } + + return _parser.OptionPrefix switch + { + CommandOptionPrefix.DoubleDash => (argument[0], argument[1]) switch + { + ('-', '-') => ParseLongOptionOrLongOptionWithValue(argument[2..]), + ('-', _) => ParseShortOptionOrMultiShortOptions(argument[1..]), + _ => ParsePositionalArgument(argument), + }, + CommandOptionPrefix.SingleDash => argument[0] switch + { + '-' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParsePositionalArgument(argument), + }, + CommandOptionPrefix.Slash => argument[0] switch + { + '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParsePositionalArgument(argument), + }, + CommandOptionPrefix.Any => (argument[0], argument[1]) switch + { + ('-', '-') => ParseLongOptionOrLongOptionWithValue(argument[2..]), + ('-', _) or ('/', _) => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParsePositionalArgument(argument), + }, + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) + { + Span separators = stackalloc char[4]; + _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + + var index = argument.IndexOfAny(separators); + if (index is 0) + { + // 没有选项名,视为位置参数。 + Type = Cat.PositionalArgument; + Value = argument; + return true; + } + if (index > 0) + { + // 带值的长选项。 + Type = Cat.LongOptionWithValue; + Option = new OptionName(true, argument[..index]); + Value = argument[(index + 1)..]; + return true; + } + // 不带值的长选项。 + Type = Cat.LongOption; + Option = new OptionName(true, argument); + return true; + } + + private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) + { + Span separators = stackalloc char[4]; + _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + + var supportsCombination = _parser.SupportsShortOptionCombination; + var supportsNoSeparator = _parser.SupportsShortOptionValueWithoutSeparator; + + var index = argument.IndexOfAny(separators); + if (index is 0) + { + // 没有选项名,视为位置参数。 + Type = Cat.PositionalArgument; + Value = argument; + return true; + } + if (index > 0) + { + // 带值的短选项。 + Type = Cat.ShortOptionWithValue; + Option = new OptionName(false, argument[..index]); + Value = argument[(index + 1)..]; + return true; + } + if (argument.Length is 1 || !supportsCombination) + { + // 单独的短选项。 + Type = Cat.ShortOption; + Option = new OptionName(false, argument); + return true; + } + if (supportsNoSeparator) + { + // 不确定是多个短选项,还是一个无分隔符的带值短选项。 + Type = Cat.MultiShortOptionsOrShortOptionConcatWithValue; + Option = new OptionName(false, argument); + return true; + } + // 多个短选项。 + Type = Cat.MultiShortOptions; + Option = new OptionName(false, argument); + return true; + } + + private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) + { + + } + + /// + /// 尝试解析布尔值。解析成功则视为选项值,失败则视为新的选项或位置参数。 + /// + /// + private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() + { + var argument = _argument; + if (argument.Length <= 4 && ( + argument.Equals("true", StringComparison.OrdinalIgnoreCase) || + argument.Equals("yes", StringComparison.OrdinalIgnoreCase) || + argument.Equals("on", StringComparison.OrdinalIgnoreCase) || + argument.Equals("1", StringComparison.OrdinalIgnoreCase) || + argument is "")) + { + Type = Cat.OptionValue; + Value = "true"; + return true; + } + if (argument.Length is > 0 and <= 5 && ( + argument.Equals("false", StringComparison.OrdinalIgnoreCase) || + argument.Equals("no", StringComparison.OrdinalIgnoreCase) || + argument.Equals("off", StringComparison.OrdinalIgnoreCase) || + argument.Equals("0", StringComparison.OrdinalIgnoreCase))) + { + Type = Cat.OptionValue; + Value = "false"; + return true; + } + return ParseOptionOrPositionalArgument(); + } + + private bool ParseCollectionOptionValueOrNewOptionOrPositionalArgument() + { + var argument = _argument.AsSpan(); + if (argument.Length is 0 or 1) + { + // 空参数或单个字符(无法组成选项),视为选项值。 + Type = Cat.OptionValue; + Value = argument; + return true; + } + + var optionPrefix = _parser.OptionPrefix; + return optionPrefix switch + { + CommandOptionPrefix.DoubleDash => (argument[0], argument[1]) switch + { + ('-', '-') => ParseLongOptionOrLongOptionWithValue(argument[2..]), + ('-', _) => ParseShortOptionOrMultiShortOptions(argument[1..]), + _ => ParseOptionValue(argument), + }, + CommandOptionPrefix.SingleDash => argument[0] switch + { + '-' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParseOptionValue(argument), + }, + CommandOptionPrefix.Slash => argument[0] switch + { + '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParseOptionValue(argument), + }, + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private bool ParseOptionValue(ReadOnlySpan argument) + { + Type = Cat.OptionValue; + Value = argument; + return true; + } + + private bool ParsePositionalArgument(ReadOnlySpan argument) + { + Type = Cat.PositionalArgument; + Value = argument; + return true; + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs new file mode 100644 index 00000000..14aecc5f --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -0,0 +1,28 @@ +namespace DotNetCampus.Cli.Utils.Parsers; + +/// +/// 命令行参数解析结果。 +/// +/// 如果解析失败,此处包含错误消息;否则为 。 +public readonly record struct CommandLineParsingResult(string? ErrorMessage) +{ + /// + /// 获取一个值,指示此解析是否成功。 + /// + public bool IsSuccess => ErrorMessage is null; + + /// + /// 创建一个表示选项未找到的解析结果。 + /// + /// 整个命令行参数列表。 + /// 当前正在解析的参数索引。 + /// 正在解析此参数的命令对象的名称。 + /// 确定没有找到的选项名称。 + /// 表示选项未找到的解析结果。 + public static CommandLineParsingResult OptionNotFound(CommandLine commandLine, int index, + string commandObjectName, ReadOnlySpan optionName) + { + var message = $"命令行对象 {commandObjectName} 不包含选项 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; + return new CommandLineParsingResult(message); + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs index 909f8416..7edde09f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs @@ -10,14 +10,14 @@ internal sealed class DotNetStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { var longOptions = new OptionDictionary(true); var shortOptions = new OptionDictionary(true); var possibleCommandNamesLength = 0; List arguments = []; - OptionName? lastOption = null; + LegacyOptionName? lastOption = null; var lastType = DotNetParsedType.Start; for (var i = 0; i < commandLineArguments.Count; i++) @@ -94,8 +94,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), longOptions, shortOptions, arguments.ToReadOnlyList()); @@ -105,7 +105,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) internal readonly ref struct DotNetArgument(DotNetParsedType type) { public DotNetParsedType Type { get; } = type; - public OptionName Option { get; private init; } + public LegacyOptionName Option { get; private init; } public ReadOnlySpan Value { get; private init; } public static DotNetArgument Parse(string argument, DotNetParsedType lastType) @@ -138,7 +138,7 @@ public static DotNetArgument Parse(string argument, DotNetParsedType lastType) if (char.IsLetterOrDigit(argument[1])) { // 短选项。 - return new DotNetArgument(DotNetParsedType.ShortOption) { Option = new OptionName(argument, Range.StartAt(1)) }; + return new DotNetArgument(DotNetParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; } throw new CommandLineParseException($"Invalid option format at index [0, 1]: {argument}"); } @@ -166,8 +166,8 @@ public static DotNetArgument Parse(string argument, DotNetParsedType lastType) return new DotNetArgument(DotNetParsedType.LongOptionWithValue) { Option = isKebabCase - ? new OptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) - : OptionName.MakeKebabCase(spans[..i], DotNetStyleParser.ConvertPascalCaseToKebabCase), + ? new LegacyOptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) + : LegacyOptionName.MakeKebabCase(spans[..i], DotNetStyleParser.ConvertPascalCaseToKebabCase), Value = spans[(i + 1)..], }; } @@ -176,8 +176,8 @@ public static DotNetArgument Parse(string argument, DotNetParsedType lastType) return new DotNetArgument(DotNetParsedType.LongOption) { Option = isKebabCase - ? new OptionName(argument, Range.StartAt(wordStartIndex)) - : OptionName.MakeKebabCase(spans, DotNetStyleParser.ConvertPascalCaseToKebabCase), + ? new LegacyOptionName(argument, Range.StartAt(wordStartIndex)) + : LegacyOptionName.MakeKebabCase(spans, DotNetStyleParser.ConvertPascalCaseToKebabCase), }; } @@ -185,7 +185,7 @@ public static DotNetArgument Parse(string argument, DotNetParsedType lastType) { // 如果是第一个参数,则后续可能是命令名或位置参数。 // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = OptionName.IsValidOptionName(argument.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); return new DotNetArgument(isValidName ? DotNetParsedType.CommandNameOrPositionalArgument : DotNetParsedType.PositionalArgument) { Value = argument.AsSpan(), diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs index 351d2a89..2c8cf3c0 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs @@ -9,7 +9,7 @@ internal sealed class FlexibleStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { var longOptions = new OptionDictionary(true); var shortOptions = new OptionDictionary(true); @@ -17,7 +17,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) List arguments = []; OptionDictionary? lastOptions = null; - OptionName? lastOption = null; + LegacyOptionName? lastOption = null; var lastType = FlexibleParsedType.Start; for (var i = 0; i < commandLineArguments.Count; i++) @@ -94,8 +94,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), longOptions, shortOptions, arguments.ToReadOnlyList()); @@ -105,7 +105,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) internal readonly ref struct FlexibleArgument(FlexibleParsedType type) { public FlexibleParsedType Type { get; } = type; - public OptionName Option { get; private init; } + public LegacyOptionName Option { get; private init; } public ReadOnlySpan Value { get; private init; } public static FlexibleArgument Parse(string argument, FlexibleParsedType lastType) @@ -138,7 +138,7 @@ public static FlexibleArgument Parse(string argument, FlexibleParsedType lastTyp if (char.IsLetterOrDigit(argument[1])) { // 短选项。 - return new FlexibleArgument(FlexibleParsedType.ShortOption) { Option = new OptionName(argument, Range.StartAt(1)) }; + return new FlexibleArgument(FlexibleParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; } throw new CommandLineParseException($"Invalid option format at index [0, 1]: {argument}"); } @@ -166,8 +166,8 @@ public static FlexibleArgument Parse(string argument, FlexibleParsedType lastTyp return new FlexibleArgument(FlexibleParsedType.LongOptionWithValue) { Option = isKebabCase - ? new OptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) - : OptionName.MakeKebabCase(spans[..i], FlexibleStyleParser.ConvertPascalCaseToKebabCase), + ? new LegacyOptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) + : LegacyOptionName.MakeKebabCase(spans[..i], FlexibleStyleParser.ConvertPascalCaseToKebabCase), Value = spans[(i + 1)..], }; } @@ -176,8 +176,8 @@ public static FlexibleArgument Parse(string argument, FlexibleParsedType lastTyp return new FlexibleArgument(FlexibleParsedType.LongOption) { Option = isKebabCase - ? new OptionName(argument, Range.StartAt(wordStartIndex)) - : OptionName.MakeKebabCase(spans, FlexibleStyleParser.ConvertPascalCaseToKebabCase), + ? new LegacyOptionName(argument, Range.StartAt(wordStartIndex)) + : LegacyOptionName.MakeKebabCase(spans, FlexibleStyleParser.ConvertPascalCaseToKebabCase), }; } @@ -186,7 +186,7 @@ public static FlexibleArgument Parse(string argument, FlexibleParsedType lastTyp { // 如果是第一个参数,则后续可能是命令名或位置参数。 // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = OptionName.IsValidOptionName(argument.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); return new FlexibleArgument(isValidName ? FlexibleParsedType.CommandNameOrPositionalArgument : FlexibleParsedType.PositionalArgument) { Value = argument.AsSpan(), diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs index c34ab634..a771a29f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs @@ -8,14 +8,14 @@ internal sealed class GnuStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = false; - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { var longOptions = new OptionDictionary(true); var shortOptions = new OptionDictionary(true); var possibleCommandNamesLength = 0; List arguments = []; - OptionName? lastOption = null; + LegacyOptionName? lastOption = null; var lastType = GnuParsedType.Start; var shortLowPriorityOptions = new Dictionary(); @@ -123,8 +123,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), longOptions, shortOptions, arguments.ToReadOnlyList()); @@ -134,7 +134,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) internal readonly ref struct GnuArgument(GnuParsedType type) { public GnuParsedType Type { get; } = type; - public OptionName Option { get; private init; } + public LegacyOptionName Option { get; private init; } public ReadOnlySpan Value { get; private init; } public static GnuArgument Parse(string argument, GnuParsedType lastType) @@ -162,11 +162,11 @@ public static GnuArgument Parse(string argument, GnuParsedType lastType) { // 带值的长选项。--option=value return new GnuArgument(GnuParsedType.LongOptionWithValue) - { Option = new OptionName(argument, new Range(2, i + 2)), Value = spans[(i + 1)..] }; + { Option = new LegacyOptionName(argument, new Range(2, i + 2)), Value = spans[(i + 1)..] }; } } // 单独的长选项。--option - return new GnuArgument(GnuParsedType.LongOption) { Option = new OptionName(argument, Range.StartAt(2)) }; + return new GnuArgument(GnuParsedType.LongOption) { Option = new LegacyOptionName(argument, Range.StartAt(2)) }; } if (!isPostPositionalArgument && argument is ['-', _, ..]) @@ -179,7 +179,7 @@ public static GnuArgument Parse(string argument, GnuParsedType lastType) throw new CommandLineParseException($"Invalid option format at index [{argument.Length}, 1]: {argument}"); } // 单独的短选项。 - return new GnuArgument(GnuParsedType.ShortOption) { Option = new OptionName(argument, Range.StartAt(1)) }; + return new GnuArgument(GnuParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; } var spans = argument.AsSpan(1); @@ -194,24 +194,24 @@ public static GnuArgument Parse(string argument, GnuParsedType lastType) { // 带值的短选项。 return new GnuArgument(GnuParsedType.ShortOptionWithValue) - { Option = new OptionName(argument, new Range(1, 2)), Value = spans[2..] }; + { Option = new LegacyOptionName(argument, new Range(1, 2)), Value = spans[2..] }; } if (!char.IsLetterOrDigit(spans[i])) { // 包含非字母或数字,说明必定是带值的短选项。-o1.txt - return new GnuArgument(GnuParsedType.ShortOptionWithValue) { Option = new OptionName(argument, new Range(1, 2)), Value = spans[1..] }; + return new GnuArgument(GnuParsedType.ShortOptionWithValue) { Option = new LegacyOptionName(argument, new Range(1, 2)), Value = spans[1..] }; } } // 多个短选项,或者带值的短选项。 return new GnuArgument(GnuParsedType.MultiShortOptionsOrShortOptionWithValue) - { Option = new OptionName(argument, Range.StartAt(1)), Value = spans[1..] }; + { Option = new LegacyOptionName(argument, Range.StartAt(1)), Value = spans[1..] }; } if (lastType is GnuParsedType.Start or GnuParsedType.CommandNameOrPositionalArgument) { // 如果是第一个参数,则后续可能是命令名或位置参数。 // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = OptionName.IsValidOptionName(argument.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); return new GnuArgument(isValidName ? GnuParsedType.CommandNameOrPositionalArgument : GnuParsedType.PositionalArgument) { Value = argument.AsSpan(), diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs index b82d62f9..f3f07573 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs @@ -2,5 +2,5 @@ internal interface ICommandLineParser { - CommandLineParsedResult Parse(IReadOnlyList commandLineArguments); + LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments); } diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsedResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs similarity index 92% rename from src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsedResult.cs rename to src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs index 652b9e95..1e829b5f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsedResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs @@ -2,7 +2,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; -internal readonly record struct CommandLineParsedResult( +internal readonly record struct LegacyCommandLineParsedResult( string PossibleCommandNames, OptionDictionary LongOptions, OptionDictionary ShortOptions, diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs new file mode 100644 index 00000000..145321d4 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs @@ -0,0 +1,19 @@ +namespace DotNetCampus.Cli.Utils.Parsers; + +internal readonly ref struct OptionName(bool isLongOption, ReadOnlySpan optionName) +{ + /// + /// 表示长选项, 表示短选项。 + /// + internal bool IsLongOption { get; } = isLongOption; + + /// + /// 选项名称,不包含前缀符号。 + /// + internal ReadOnlySpan Name { get; } = optionName; + + public override string ToString() + { + return Name.ToString(); + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs index 351fc145..39e63f51 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs @@ -8,13 +8,13 @@ internal sealed class PosixStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = false; - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { var shortOptions = new OptionDictionary(true); var possibleCommandNamesLength = 0; List arguments = []; - OptionName? lastOption = null; + LegacyOptionName? lastOption = null; var lastType = PosixParsedType.Start; for (var i = 0; i < commandLineArguments.Count; i++) @@ -80,8 +80,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), OptionDictionary.Empty, // POSIX 风格不支持长选项 shortOptions, arguments.ToReadOnlyList()); @@ -91,7 +91,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) internal readonly ref struct PosixArgument(PosixParsedType type) { public PosixParsedType Type { get; } = type; - public OptionName Option { get; private init; } + public LegacyOptionName Option { get; private init; } public ReadOnlySpan Value { get; private init; } public static PosixArgument Parse(string argument, PosixParsedType lastType) @@ -120,7 +120,7 @@ public static PosixArgument Parse(string argument, PosixParsedType lastType) throw new CommandLineParseException($"Invalid option format at index [{argument.Length}, 1]: {argument}"); } // 单独的短选项。 - return new PosixArgument(PosixParsedType.ShortOption) { Option = new OptionName(argument, Range.StartAt(1)) }; + return new PosixArgument(PosixParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; } // 检查所有字符是否都是有效的选项字符 @@ -133,14 +133,14 @@ public static PosixArgument Parse(string argument, PosixParsedType lastType) } // 多个短选项,如 -abc - return new PosixArgument(PosixParsedType.MultiShortOptions) { Option = new OptionName(argument, Range.StartAt(1)) }; + return new PosixArgument(PosixParsedType.MultiShortOptions) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; } if (lastType is PosixParsedType.Start or PosixParsedType.CommandNameOrPositionalArgument) { // 如果是第一个参数,则后续可能是命令名或位置参数。 // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = OptionName.IsValidOptionName(argument.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); return new PosixArgument(isValidName ? PosixParsedType.CommandNameOrPositionalArgument : PosixParsedType.PositionalArgument) { Value = argument.AsSpan(), diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs index c2e06861..5de08f89 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs @@ -7,13 +7,13 @@ internal sealed class PowerShellStyleParser : ICommandLineParser { internal static bool ConvertPascalCaseToKebabCase { get; } = true; - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { var longOptions = new OptionDictionary(true); var possibleCommandNamesLength = 0; List arguments = []; - OptionName? lastOption = null; + LegacyOptionName? lastOption = null; var lastType = PowerShellParsedType.Start; for (var i = 0; i < commandLineArguments.Count; i++) @@ -62,8 +62,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), longOptions, // PowerShell 风格不使用短选项,所以直接使用空字典。 OptionDictionary.Empty, @@ -74,7 +74,7 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) internal readonly ref struct PowerShellArgument(PowerShellParsedType type) { public PowerShellParsedType Type { get; } = type; - public OptionName Option { get; private init; } + public LegacyOptionName Option { get; private init; } public ReadOnlySpan Value { get; private init; } public static PowerShellArgument Parse(string argument, PowerShellParsedType lastType) @@ -99,7 +99,7 @@ public static PowerShellArgument Parse(string argument, PowerShellParsedType las var optionSpan = argument.AsSpan(1); return new PowerShellArgument(PowerShellParsedType.Option) { - Option = OptionName.MakeKebabCase(optionSpan, PowerShellStyleParser.ConvertPascalCaseToKebabCase), + Option = LegacyOptionName.MakeKebabCase(optionSpan, PowerShellStyleParser.ConvertPascalCaseToKebabCase), }; } @@ -108,7 +108,7 @@ public static PowerShellArgument Parse(string argument, PowerShellParsedType las { // 如果是第一个参数,则后续可能是命令名或位置参数。 // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = OptionName.IsValidOptionName(argument.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); return new PowerShellArgument(isValidName ? PowerShellParsedType.CommandNameOrPositionalArgument : PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan(), diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs index 386b8771..4a95dec9 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs @@ -24,7 +24,7 @@ public UrlStyleParser(string scheme) _scheme = scheme; } - public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) + public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) { if (commandLineArguments.Count is not 1) { @@ -89,8 +89,8 @@ public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) } } - return new CommandLineParsedResult( - CommandLineParsedResult.MakePossibleCommandNames(possibleCommandNames, ConvertPascalCaseToKebabCase), + return new LegacyCommandLineParsedResult( + LegacyCommandLineParsedResult.MakePossibleCommandNames(possibleCommandNames, ConvertPascalCaseToKebabCase), longOptions, shortOptions, arguments.ToReadOnlyList()); @@ -135,7 +135,7 @@ public static UrlPart ReadNext(string url, ref int index, UrlParsedType lastType } var value = HttpUtility.UrlDecode(url.AsSpan(startIndex, endIndex - startIndex).ToString()); - var isValidName = OptionName.IsValidOptionName(value.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(value.AsSpan()); return new UrlPart(isValidName ? UrlParsedType.CommandNameOrPositionalArgument : UrlParsedType.PositionalArgument) { Value = value, @@ -214,7 +214,7 @@ private static UrlPart ReadNextPositionalArgument(string url, ref int index, Url index = endIndex; } var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString()); - var isValidName = OptionName.IsValidOptionName(value.AsSpan()); + var isValidName = LegacyOptionName.IsValidOptionName(value.AsSpan()); var type = lastType is UrlParsedType.PositionalArgument ? UrlParsedType.PositionalArgument : UrlParsedType.CommandNameOrPositionalArgument; @@ -243,7 +243,7 @@ private static UrlPart ReadNextParameterName(string url, ref int index) index = endIndex; return new UrlPart(UrlParsedType.ParameterName) { - Name = OptionName.MakeKebabCase(value + Name = LegacyOptionName.MakeKebabCase(value #if !NETCOREAPP3_1_OR_GREATER .AsSpan() #endif From d90b88ad86b42f58571a0bea64c79f74723554f4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 14 Sep 2025 13:25:29 +0800 Subject: [PATCH 005/193] =?UTF-8?q?=E7=BB=88=E4=BA=8E=E6=8A=8A=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E5=91=BD=E4=BB=A4=E8=A1=8C=E8=A7=A3=E6=9E=90=E5=99=A8?= =?UTF-8?q?=E5=86=99=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 27 ++- .../Utils/Parsers/Callbacks.cs | 39 +++- .../Utils/Parsers/CommandArgumentType.cs | 5 + .../Utils/Parsers/CommandLineParser.cs | 221 +++++++++++++----- .../Utils/Parsers/CommandLineParsingResult.cs | 31 +++ 5 files changed, 250 insertions(+), 73 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 50d52b42..5dcc7fb7 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -21,7 +21,6 @@ public readonly record struct CommandLineParsingOptions OptionPrefix = CommandOptionPrefix.Any, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), - DictionaryValueSeparators = CommandSeparatorChars.Create('='), }, }; @@ -39,7 +38,6 @@ public readonly record struct CommandLineParsingOptions OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), - DictionaryValueSeparators = CommandSeparatorChars.Create('='), }, }; @@ -57,7 +55,6 @@ public readonly record struct CommandLineParsingOptions OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create('=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), - DictionaryValueSeparators = CommandSeparatorChars.Create('='), }, }; @@ -75,7 +72,6 @@ public readonly record struct CommandLineParsingOptions OptionPrefix = CommandOptionPrefix.SingleDash, OptionValueSeparators = CommandSeparatorChars.Create(' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), - DictionaryValueSeparators = CommandSeparatorChars.Create('='), }, }; @@ -93,7 +89,6 @@ public readonly record struct CommandLineParsingOptions OptionPrefix = CommandOptionPrefix.Slash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), - DictionaryValueSeparators = CommandSeparatorChars.Create('='), }, }; @@ -239,12 +234,6 @@ public bool SupportsShortOptionValueWithoutSeparator /// 如 ',', ';', ' ' 分别对应: --option value1,value2, --option value1;value2, --option value1 value2。 ///
public CommandSeparatorChars CollectionValueSeparators { get; init; } - - /// - /// 允许用户使用哪些分隔符来分隔字典类型的选项值中的键和值。
- /// 如 '=', ':' 分别对应: --option key=value, --option key:value。 - ///
- public CommandSeparatorChars DictionaryValueSeparators { get; init; } } /// @@ -340,3 +329,19 @@ public enum OptionValueType : byte /// NotExist, } + +/// +/// 位置参数值的类型。此枚举中的位置参数值类型会影响到位置参数值的解析方式。 +/// +public enum PositionalArgumentValueType : byte +{ + /// + /// 正常的位置参数。 + /// + Normal, + + /// + /// 指定位置处的位置参数没有匹配到任何位置参数范围。 + /// + NotExist, +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs index b1381775..2d2c89ed 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs @@ -8,19 +8,46 @@ namespace DotNetCampus.Cli.Utils.Parsers; public delegate bool CheckIsCommandCallback(int argumentIndex); /// -/// 要求源生成器匹配长名称,返回此长选项的值类型。 +/// 要求源生成器匹配长名称,返回此长选项的值类型和追加值的回调。 /// /// 由用户输入的长名称(已去掉前缀符号和后续所带的值,未处理命名法变换)。 /// 如果此参数未指定大小写敏感性,则使用此默认值。 /// 由开发者配置的允许的命名法。 -/// 此长选项的值类型。 -public delegate OptionValueType LongOptionMatchingCallback(ReadOnlySpan longOption, bool defaultCaseSensitive, +/// 此长选项的匹配结果。 +public delegate OptionValueHandler LongOptionMatchingCallback(ReadOnlySpan longOption, bool defaultCaseSensitive, params ReadOnlySpan allowedNamingPolicies); /// -/// 要求源生成器匹配短名称,返回此短选项的值类型。 +/// 要求源生成器匹配短名称,返回此短选项的值类型和追加值的回调。 /// /// 由用户输入的短名称(已去掉前缀符号和后续所带的值,包含多个字符时也只允许匹配一个短选项)。 /// 如果此参数未指定大小写敏感性,则使用此默认值。 -/// 此短选项的值类型。 -public delegate OptionValueType ShortOptionMatchingCallback(ReadOnlySpan shortOption, bool defaultCaseSensitive); +/// 此短选项的匹配结果。 +public delegate OptionValueHandler ShortOptionMatchingCallback(ReadOnlySpan shortOption, bool defaultCaseSensitive); + +/// +/// 要求源生成器匹配位置参数,返回此位置参数的范围和追加值的回调。 +/// +/// 由用户输入的位置参数的值。 +/// 位置参数的索引(从 0 开始)。 +/// 此位置参数的匹配结果。 +public delegate PositionalArgumentValueHandler PositionalArgumentMatchingCallback(ReadOnlySpan value, int argumentIndex); + +/// +/// 向某个选项或位置参数追加一个值的回调。 +/// +public delegate void AppendValueCallback(ReadOnlySpan value); + +/// +/// 源生成器匹配属性的匹配结果。 +/// +/// 此选项的值类型。 +/// 向此选项追加一个值的回调。 +public readonly record struct OptionValueHandler(OptionValueType ValueType, AppendValueCallback AppendValue); + +/// +/// 源生成器匹配位置参数的匹配结果。 +/// +/// 此位置参数的值类型。 +/// 向此位置参数追加一个值的回调。 +public readonly record struct PositionalArgumentValueHandler(PositionalArgumentValueType ValueType, AppendValueCallback AppendValue); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs index 470ed7f7..2e7cb792 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs @@ -50,6 +50,11 @@ internal enum CommandArgumentType ///
OptionWithValue, + /// + /// 无法解析的选项。 + /// + ErrorOption, + /// /// 多个短选项。-abc /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 76e8be32..613b44ce 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -9,6 +9,8 @@ public readonly ref struct CommandLineParser { private readonly CommandLine _commandLine; private readonly string _commandObjectName; + private readonly bool _caseSensitive; + private readonly CommandNamingPolicy _namingPolicy; /// /// 通用的命令行参数解析器。(此解析器不可解析 URL 类型的参数。) @@ -20,29 +22,33 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName) _commandLine = commandLine; _commandObjectName = commandObjectName; Style = commandLine.ParsingOptions.Style; - NamingPolicy = Style.NamingPolicy; + _namingPolicy = Style.NamingPolicy; OptionPrefix = Style.OptionPrefix; - CaseSensitive = Style.CaseSensitive; + _caseSensitive = Style.CaseSensitive; SupportsLongOption = Style.SupportsLongOption; SupportsShortOption = Style.SupportsShortOption; SupportsShortOptionCombination = Style.SupportsShortOptionCombination; SupportsShortOptionValueWithoutSeparator = Style.SupportsShortOptionValueWithoutSeparator; } + /// + /// 获取解析命令行时所使用的各种选项。 + /// internal CommandLineStyleDetails Style { get; } - internal CommandNamingPolicy NamingPolicy { get; } - + /// internal CommandOptionPrefix OptionPrefix { get; } - internal bool CaseSensitive { get; } - + /// internal bool SupportsLongOption { get; } + /// internal bool SupportsShortOption { get; } + /// internal bool SupportsShortOptionCombination { get; } + /// internal bool SupportsShortOptionValueWithoutSeparator { get; } /// @@ -60,12 +66,23 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName) /// public required ShortOptionMatchingCallback MatchShortOption { get; init; } + /// + /// 要求源生成器匹配位置参数,返回位置参数的范围。 + /// + public required PositionalArgumentMatchingCallback MatchPositionalArguments { get; init; } + + /// + /// 解析命令行参数,并返回解析结果。 + /// + /// 命令行参数解析结果。 public CommandLineParsingResult Parse() { var arguments = _commandLine.CommandLineArguments; - var lastOptionName = new OptionName(false, []); - var lastOptionType = OptionValueType.Normal; - var lastType = Cat.Start; + var currentOptionName = new OptionName(false, []); + var currentOptionType = OptionValueType.Normal; + AppendValueCallback currentOptionAppender = DefaultAppender; + var currentPositionArgumentIndex = 0; + var lastState = Cat.Start; for (var index = 0; index < arguments.Count; index++) { @@ -76,57 +93,131 @@ public CommandLineParsingResult Parse() continue; } - // 解析当前参数。 - var part = new CommandArgumentPart(this, index, argument, lastType, lastOptionType); + // 状态机状态转移。 + var part = new CommandArgumentPart(this, argument, lastState, currentOptionType); part.Parse(); - var (currentType, optionName, value) = part; + var (state, optionName, value) = part; + lastState = state; - // 更新状态。 - lastType = currentType; - if (currentType is Cat.LongOption or Cat.ShortOption or Cat.Option) + // 应用新状态下的值。 + switch (state) { - // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 - lastOptionName = optionName; - var optionType = currentType switch + case Cat.LongOption or Cat.ShortOption or Cat.Option: { - Cat.LongOption => MatchLongOption(optionName.Name, CaseSensitive, NamingPolicy), - Cat.ShortOption => MatchShortOption(optionName.Name, CaseSensitive), - _ => MatchLongOption(optionName.Name, CaseSensitive, NamingPolicy) switch + // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 + currentOptionName = optionName; + var (optionType, appender) = state switch { - OptionValueType.NotExist => MatchShortOption(optionName.Name, CaseSensitive), - var t => t, - }, - }; - if (optionType is OptionValueType.NotExist) + Cat.LongOption => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), + Cat.ShortOption => MatchShortOption(optionName.Name, _caseSensitive), + _ => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy) switch + { + { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName.Name, _caseSensitive), + var t => t, + }, + }; + if (optionType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + } + currentOptionType = optionType; + currentOptionAppender = appender; + break; + } + case Cat.OptionValue: { - // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + currentOptionAppender(value); + if (currentOptionType is not OptionValueType.Collection) + { + // 如果不是集合,那么此选项已经结束。 + // 清空上一个选项,避免误用。 + currentOptionName = new OptionName(false, []); + currentOptionType = OptionValueType.Normal; + currentOptionAppender = DefaultAppender; + } + break; } - lastOptionType = optionType; - } - else if (currentType is Cat.OptionValue && lastOptionType is OptionValueType.Collection) - { - // 如果当前是选项值,且上个选项是一个集合值,则继续使用上个选项。 - } - else - { - // 其他情况,都需要清空上一个选项,避免误用。 - lastOptionName = new OptionName(false, []); - lastOptionType = OptionValueType.Normal; + case Cat.PositionalArgument or Cat.PostPositionalArgument: + { + var (positionalArgumentType, appender) = MatchPositionalArguments(value, currentPositionArgumentIndex); + if (positionalArgumentType is PositionalArgumentValueType.NotExist) + { + // 如果位置参数不存在,则报告错误。 + return CommandLineParsingResult.PositionalArgumentNotFound(_commandLine, index, _commandObjectName, currentPositionArgumentIndex); + } + currentPositionArgumentIndex++; + appender(value); + break; + } + case Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue: + { + currentOptionAppender(value); + break; + } + case Cat.ErrorOption: + // 如果当前参数疑似选项但解析失败,则报告错误。 + return CommandLineParsingResult.OptionParseError(_commandLine, index); + case Cat.MultiShortOptions: + { + // 逐个处理多个短选项。 + for (var i = 0; i < optionName.Name.Length; i++) + { + var n = optionName.Name[i..(i + 1)]; + var (optionType, appender) = MatchShortOption(n, _caseSensitive); + if (optionType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); + } + appender([]); + } + break; + } + case Cat.MultiShortOptionsOrShortOptionConcatWithValue: + { + // 先看看是否是一个多字符短选项,如果不是,再看看是否是单个字符无分隔符带值的短选项。 + var m = optionName.Name; + var (optionType, appender) = MatchShortOption(m, _caseSensitive); + if (optionType is not OptionValueType.NotExist) + { + // 是一个多字符短选项。 + appender([]); + break; + } + // 不是一个多字符短选项,尝试解析为单个字符无分隔符带值的短选项。 + var n = m[..1]; + var v = m[1..]; + (optionType, appender) = MatchShortOption(n, _caseSensitive); + if (optionType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); + } + appender(v); + break; + } + default: + // 其他状态要么已经处理过了,要不还未处理,要么不需要处理,所以不需要做任何事情。 + break; } - - // 处理解析结果。 } + + return CommandLineParsingResult.Success; + } + + private static void DefaultAppender(ReadOnlySpan value) + { + throw new InvalidOperationException("不可能有机会调用到这个默认的追加值回调。"); } } /// -/// 辅助解析命令行参数中的其中一个参数。 +/// 这是命令行解析状态机中的其中一个状态。当调用 方法后,此对象会被修改以转移到新的状态。 /// internal ref struct CommandArgumentPart { private readonly CommandLineParser _parser; - private readonly int _index; private readonly string _argument; private readonly Cat _lastType; private readonly OptionValueType _lastOptionType; @@ -135,14 +226,12 @@ internal ref struct CommandArgumentPart /// 辅助解析命令行参数中的其中一个参数。 /// /// 正在使用的命令行参数解析器。 - /// 正在解析的参数的索引。 /// 要解析的参数。 /// 上一个参数的类型,初始为 。 /// 上一个参数的选项值类型,如果上一个参数不是选项,则为默认值。 - public CommandArgumentPart(CommandLineParser parser, int index, string argument, Cat lastType, OptionValueType lastOptionType) + public CommandArgumentPart(CommandLineParser parser, string argument, Cat lastType, OptionValueType lastOptionType) { _parser = parser; - _index = index; _argument = argument; _lastType = lastType; _lastOptionType = lastOptionType; @@ -177,12 +266,12 @@ public void Deconstruct(out Cat type, out OptionName optionName, out ReadOnlySpa } /// - /// 开始解析这个参数,可通过解构获得解析结果。 + /// 以上一个状态为基准,解析当前参数,并转移到新的状态。 /// /// /// 返回值没有意义,纯粹为了使用 switch 表达式。 /// - public bool Parse() => _lastType switch + public void Parse() => _ = _lastType switch { Cat.Start or Cat.Command => ParseCommandRegion(), Cat.PositionalArgumentSeparator or Cat.PostPositionalArgument => ParsePostPositionalArgumentRegion(), @@ -296,9 +385,8 @@ private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) var index = argument.IndexOfAny(separators); if (index is 0) { - // 没有选项名,视为位置参数。 - Type = Cat.PositionalArgument; - Value = argument; + // 没有选项名,报告错误。 + Type = Cat.ErrorOption; return true; } if (index > 0) @@ -327,9 +415,8 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) var index = argument.IndexOfAny(separators); if (index is 0) { - // 没有选项名,视为位置参数。 - Type = Cat.PositionalArgument; - Value = argument; + // 没有选项名,报告错误。 + Type = Cat.ErrorOption; return true; } if (index > 0) @@ -362,7 +449,29 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) { + Span separators = stackalloc char[4]; + _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + var index = argument.IndexOfAny(separators); + if (index is 0) + { + // 没有选项名,报告错误。 + Type = Cat.ErrorOption; + return true; + } + if (index > 0) + { + // 带值的选项。 + Type = Cat.OptionWithValue; + Option = new OptionName(true, argument[..index]); + Value = argument[(index + 1)..]; + return true; + } + // 不带值的选项。 + Type = Cat.Option; + Option = new OptionName(true, argument); + return true; } /// @@ -380,7 +489,7 @@ private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() argument is "")) { Type = Cat.OptionValue; - Value = "true"; + Value = "true".AsSpan(); return true; } if (argument.Length is > 0 and <= 5 && ( @@ -390,7 +499,7 @@ private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() argument.Equals("0", StringComparison.OrdinalIgnoreCase))) { Type = Cat.OptionValue; - Value = "false"; + Value = "false".AsSpan(); return true; } return ParseOptionOrPositionalArgument(); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs index 14aecc5f..a244561e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -11,6 +11,11 @@ public readonly record struct CommandLineParsingResult(string? ErrorMessage) /// public bool IsSuccess => ErrorMessage is null; + /// + /// 获取一个表示成功的解析结果。 + /// + public static CommandLineParsingResult Success => new(null); + /// /// 创建一个表示选项未找到的解析结果。 /// @@ -25,4 +30,30 @@ public static CommandLineParsingResult OptionNotFound(CommandLine commandLine, i var message = $"命令行对象 {commandObjectName} 不包含选项 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; return new CommandLineParsingResult(message); } + + /// + /// 创建一个表示选项未找到的解析结果。 + /// + /// 整个命令行参数列表。 + /// 当前正在解析的参数索引。 + /// 表示选项未找到的解析结果。 + public static CommandLineParsingResult OptionParseError(CommandLine commandLine, int index) + { + var message = $"参数 {commandLine.CommandLineArguments[index]} 未能解析出选项名。参数列表:{commandLine},索引 {index}。"; + return new CommandLineParsingResult(message); + } + + /// + /// 创建一个表示位置参数未找到的解析结果。 + /// + /// 整个命令行参数列表。 + /// 当前正在解析的参数索引。 + /// 正在解析此参数的命令对象的名称。 + /// 要查找的位置参数的索引。 + /// 表示位置参数未找到的解析结果。 + public static CommandLineParsingResult PositionalArgumentNotFound(CommandLine commandLine, int index, string commandObjectName, int positionalArgumentIndex) + { + var message = $"命令行对象 {commandObjectName} 位置参数范围不包含索引 {positionalArgumentIndex}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; + return new CommandLineParsingResult(message); + } } From 1623465bb0493a76db1861f9fb989f7c6459a52e Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 11:59:30 +0800 Subject: [PATCH 006/193] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E6=AF=8F=E4=B8=AA=E5=B1=9E=E6=80=A7=E8=B5=8B=E5=80=BC=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E8=A3=85=E7=AE=B1=E5=92=8C=E5=A7=94=E6=89=98=E5=88=9B?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 5 ++ .../Utils/Parsers/Callbacks.cs | 39 ++++---- .../Utils/Parsers/CommandLineParser.cs | 88 ++++++++++--------- 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 5dcc7fb7..9a44b5c3 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -324,6 +324,11 @@ public enum OptionValueType : byte ///
Collection, + /// + /// 字典值。会尝试解析多个键值对,直到遇到下一个选项或位置参数分隔符为止。 + /// + Dictionary, + /// /// 用户输入的选项没有命中到任何已知的选项。 /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs index 2d2c89ed..68c693f0 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs @@ -1,21 +1,13 @@ namespace DotNetCampus.Cli.Utils.Parsers; -/// -/// 要求源生成器判断某个索引处的参数是否为命令(主命令、子命令或多级子命令)。 -/// -/// 要判断的参数的索引。 -/// 如果此参数为命令(主命令、子命令或多级子命令),则返回 ;否则返回 -public delegate bool CheckIsCommandCallback(int argumentIndex); - /// /// 要求源生成器匹配长名称,返回此长选项的值类型和追加值的回调。 /// /// 由用户输入的长名称(已去掉前缀符号和后续所带的值,未处理命名法变换)。 /// 如果此参数未指定大小写敏感性,则使用此默认值。 -/// 由开发者配置的允许的命名法。 +/// 由开发者配置的允许的命名法。 /// 此长选项的匹配结果。 -public delegate OptionValueHandler LongOptionMatchingCallback(ReadOnlySpan longOption, bool defaultCaseSensitive, - params ReadOnlySpan allowedNamingPolicies); +public delegate OptionValueMatch LongOptionMatchingCallback(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy); /// /// 要求源生成器匹配短名称,返回此短选项的值类型和追加值的回调。 @@ -23,7 +15,7 @@ public delegate OptionValueHandler LongOptionMatchingCallback(ReadOnlySpan /// 由用户输入的短名称(已去掉前缀符号和后续所带的值,包含多个字符时也只允许匹配一个短选项)。 /// 如果此参数未指定大小写敏感性,则使用此默认值。 /// 此短选项的匹配结果。 -public delegate OptionValueHandler ShortOptionMatchingCallback(ReadOnlySpan shortOption, bool defaultCaseSensitive); +public delegate OptionValueMatch ShortOptionMatchingCallback(ReadOnlySpan shortOption, bool defaultCaseSensitive); /// /// 要求源生成器匹配位置参数,返回此位置参数的范围和追加值的回调。 @@ -31,23 +23,36 @@ public delegate OptionValueHandler LongOptionMatchingCallback(ReadOnlySpan /// 由用户输入的位置参数的值。 /// 位置参数的索引(从 0 开始)。 /// 此位置参数的匹配结果。 -public delegate PositionalArgumentValueHandler PositionalArgumentMatchingCallback(ReadOnlySpan value, int argumentIndex); +public delegate PositionalArgumentValueMatch PositionalArgumentMatchingCallback(ReadOnlySpan value, int argumentIndex); /// /// 向某个选项或位置参数追加一个值的回调。 /// -public delegate void AppendValueCallback(ReadOnlySpan value); +/// 要追加的键(对于字典类型的选项有效,其他类型永远为空)。 +/// 要追加的值。 +public delegate void AppendValueCallback(ReadOnlySpan key, ReadOnlySpan value); + +/// +/// 向指定索引处的属性赋值。 +/// +/// 要赋值的属性名称(调试追踪用)。 +/// 要赋值的属性索引(源生成器生成的索引)。 +/// 要赋值的键(对于字典类型的选项有效,其他类型永远为空)。 +/// 要赋值的值。 +public delegate void AssignPropertyValueCallback(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value); /// /// 源生成器匹配属性的匹配结果。 /// +/// 此选项对应的属性名称。 +/// 此选项对应的属性索引。 /// 此选项的值类型。 -/// 向此选项追加一个值的回调。 -public readonly record struct OptionValueHandler(OptionValueType ValueType, AppendValueCallback AppendValue); +public readonly record struct OptionValueMatch(string PropertyName, int PropertyIndex, OptionValueType ValueType); /// /// 源生成器匹配位置参数的匹配结果。 /// +/// 此选项对应的属性名称。 +/// 此选项对应的属性索引。 /// 此位置参数的值类型。 -/// 向此位置参数追加一个值的回调。 -public readonly record struct PositionalArgumentValueHandler(PositionalArgumentValueType ValueType, AppendValueCallback AppendValue); +public readonly record struct PositionalArgumentValueMatch(string PropertyName, int PropertyIndex, PositionalArgumentValueType ValueType); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 613b44ce..60f7dec7 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -9,6 +9,7 @@ public readonly ref struct CommandLineParser { private readonly CommandLine _commandLine; private readonly string _commandObjectName; + private readonly int _commandCount; private readonly bool _caseSensitive; private readonly CommandNamingPolicy _namingPolicy; @@ -17,10 +18,12 @@ public readonly ref struct CommandLineParser /// /// 要解析的命令行参数。 /// 正在解析此参数的命令对象的名称。 - public CommandLineParser(CommandLine commandLine, string commandObjectName) + /// 主命令/子命令/多级子命令的数量。在解析时,要跳过这些命令。 + public CommandLineParser(CommandLine commandLine, string commandObjectName, int commandCount) { _commandLine = commandLine; _commandObjectName = commandObjectName; + _commandCount = commandCount; Style = commandLine.ParsingOptions.Style; _namingPolicy = Style.NamingPolicy; OptionPrefix = Style.OptionPrefix; @@ -51,11 +54,6 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName) /// internal bool SupportsShortOptionValueWithoutSeparator { get; } - /// - /// 要求源生成器判断某个索引处的参数是否为命令(主命令、子命令或多级子命令)。 - /// - public required CheckIsCommandCallback IsCommand { get; init; } - /// /// 要求源生成器匹配长名称,返回此长选项的值类型。 /// @@ -71,6 +69,16 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName) /// public required PositionalArgumentMatchingCallback MatchPositionalArguments { get; init; } + /// + /// 要求源生成器将解析到的值赋值给指定索引处的属性。 + /// + public required AssignPropertyValueCallback AssignPropertyValue { get; init; } + + /// + /// 获取默认的选项值处理器(默认的选项处理器仅为了避免代码错误产生误用,实际永远不会被使用)。 + /// + private static OptionValueMatch DefaultOptionValueHandler => new OptionValueMatch("", -1, OptionValueType.Normal); + /// /// 解析命令行参数,并返回解析结果。 /// @@ -79,22 +87,16 @@ public CommandLineParsingResult Parse() { var arguments = _commandLine.CommandLineArguments; var currentOptionName = new OptionName(false, []); - var currentOptionType = OptionValueType.Normal; - AppendValueCallback currentOptionAppender = DefaultAppender; + var currentOption = new OptionValueMatch("", -1, OptionValueType.Normal); var currentPositionArgumentIndex = 0; var lastState = Cat.Start; - for (var index = 0; index < arguments.Count; index++) + for (var index = _commandCount; index < arguments.Count; index++) { - // 跳过命令(主命令、子命令、多级子命令)。 var argument = arguments[index]; - if (IsCommand(index)) - { - continue; - } // 状态机状态转移。 - var part = new CommandArgumentPart(this, argument, lastState, currentOptionType); + var part = new CommandArgumentPart(this, argument, lastState, currentOption.ValueType); part.Parse(); var (state, optionName, value) = part; lastState = state; @@ -106,7 +108,7 @@ public CommandLineParsingResult Parse() { // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 currentOptionName = optionName; - var (optionType, appender) = state switch + var optionMatch = state switch { Cat.LongOption => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), Cat.ShortOption => MatchShortOption(optionName.Name, _caseSensitive), @@ -116,61 +118,61 @@ public CommandLineParsingResult Parse() var t => t, }, }; - if (optionType is OptionValueType.NotExist) + if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); } - currentOptionType = optionType; - currentOptionAppender = appender; + currentOption = optionMatch; break; } case Cat.OptionValue: { - currentOptionAppender(value); - if (currentOptionType is not OptionValueType.Collection) + AssignPropertyValue(currentOption.PropertyName, currentOption.PropertyIndex, [], value); + if (currentOption.ValueType is not OptionValueType.Collection) { // 如果不是集合,那么此选项已经结束。 // 清空上一个选项,避免误用。 currentOptionName = new OptionName(false, []); - currentOptionType = OptionValueType.Normal; - currentOptionAppender = DefaultAppender; + currentOption = DefaultOptionValueHandler; } break; } case Cat.PositionalArgument or Cat.PostPositionalArgument: { - var (positionalArgumentType, appender) = MatchPositionalArguments(value, currentPositionArgumentIndex); - if (positionalArgumentType is PositionalArgumentValueType.NotExist) + var positionalArgumentMatch = MatchPositionalArguments(value, currentPositionArgumentIndex); + if (positionalArgumentMatch.ValueType is PositionalArgumentValueType.NotExist) { // 如果位置参数不存在,则报告错误。 return CommandLineParsingResult.PositionalArgumentNotFound(_commandLine, index, _commandObjectName, currentPositionArgumentIndex); } currentPositionArgumentIndex++; - appender(value); + AssignPropertyValue(positionalArgumentMatch.PropertyName, positionalArgumentMatch.PropertyIndex, [], value); break; } case Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue: { - currentOptionAppender(value); + AssignPropertyValue(currentOption.PropertyName, currentOption.PropertyIndex, [], value); break; } case Cat.ErrorOption: + { // 如果当前参数疑似选项但解析失败,则报告错误。 return CommandLineParsingResult.OptionParseError(_commandLine, index); + } case Cat.MultiShortOptions: { // 逐个处理多个短选项。 for (var i = 0; i < optionName.Name.Length; i++) { var n = optionName.Name[i..(i + 1)]; - var (optionType, appender) = MatchShortOption(n, _caseSensitive); - if (optionType is OptionValueType.NotExist) + var optionMatch = MatchShortOption(n, _caseSensitive); + if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); } - appender([]); + AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], []); } break; } @@ -178,38 +180,35 @@ public CommandLineParsingResult Parse() { // 先看看是否是一个多字符短选项,如果不是,再看看是否是单个字符无分隔符带值的短选项。 var m = optionName.Name; - var (optionType, appender) = MatchShortOption(m, _caseSensitive); - if (optionType is not OptionValueType.NotExist) + var optionMatch = MatchShortOption(m, _caseSensitive); + if (optionMatch.ValueType is not OptionValueType.NotExist) { // 是一个多字符短选项。 - appender([]); + AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], []); break; } // 不是一个多字符短选项,尝试解析为单个字符无分隔符带值的短选项。 var n = m[..1]; var v = m[1..]; - (optionType, appender) = MatchShortOption(n, _caseSensitive); - if (optionType is OptionValueType.NotExist) + optionMatch = MatchShortOption(n, _caseSensitive); + if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); } - appender(v); + AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], v); break; } default: + { // 其他状态要么已经处理过了,要不还未处理,要么不需要处理,所以不需要做任何事情。 break; + } } } return CommandLineParsingResult.Success; } - - private static void DefaultAppender(ReadOnlySpan value) - { - throw new InvalidOperationException("不可能有机会调用到这个默认的追加值回调。"); - } } /// @@ -313,7 +312,7 @@ private bool ParseOptionAndPositionalArgumentRegion() // 如果是布尔选项,则后面只能跟布尔值,否则只能是新的选项或位置参数。 OptionValueType.Boolean => ParseBooleanOptionValueOrNewOptionOrPositionalArgument(), // 如果是集合选项,则后面可以跟多个值,直到遇到新的选项或位置参数分隔符为止。 - OptionValueType.Collection => ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), + OptionValueType.Collection or OptionValueType.Dictionary => ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), // 如果是普通选项,则后面只能是选项值。 _ => ParseOptionValue(_argument.AsSpan()), }), @@ -342,6 +341,7 @@ private bool ParseOptionOrPositionalArgument() var argument = _argument.AsSpan(); if (argument.Length is 0 or 1) { + // TODO 针对单独有选项分隔符的,要报错。 // 空参数或单个字符(无法组成选项),视为位置参数。 Type = Cat.PositionalArgument; Value = argument; @@ -434,6 +434,10 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) Option = new OptionName(false, argument); return true; } + // TODO 不可能三种都存在 + // -abc -a -b -c + // -abc + // -abc -a bc if (supportsNoSeparator) { // 不确定是多个短选项,还是一个无分隔符的带值短选项。 From 418fba5a2636116db888ccb0beca0bbfd3c7bdd2 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 12:10:46 +0800 Subject: [PATCH 007/193] =?UTF-8?q?=E8=83=BD=E8=B5=8B=E5=80=BC=E5=AD=97?= =?UTF-8?q?=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 60f7dec7..04b71dfc 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -128,7 +128,7 @@ public CommandLineParsingResult Parse() } case Cat.OptionValue: { - AssignPropertyValue(currentOption.PropertyName, currentOption.PropertyIndex, [], value); + AssignOptionValue(currentOption, value); if (currentOption.ValueType is not OptionValueType.Collection) { // 如果不是集合,那么此选项已经结束。 @@ -147,12 +147,12 @@ public CommandLineParsingResult Parse() return CommandLineParsingResult.PositionalArgumentNotFound(_commandLine, index, _commandObjectName, currentPositionArgumentIndex); } currentPositionArgumentIndex++; - AssignPropertyValue(positionalArgumentMatch.PropertyName, positionalArgumentMatch.PropertyIndex, [], value); + AssignPositionalArgumentValue(positionalArgumentMatch, value); break; } case Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue: { - AssignPropertyValue(currentOption.PropertyName, currentOption.PropertyIndex, [], value); + AssignOptionValue(currentOption, value); break; } case Cat.ErrorOption: @@ -172,7 +172,7 @@ public CommandLineParsingResult Parse() // 如果选项不存在,则报告错误。 return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); } - AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], []); + AssignOptionValue(optionMatch, []); } break; } @@ -184,7 +184,7 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is not OptionValueType.NotExist) { // 是一个多字符短选项。 - AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], []); + AssignOptionValue(optionMatch, []); break; } // 不是一个多字符短选项,尝试解析为单个字符无分隔符带值的短选项。 @@ -196,7 +196,7 @@ public CommandLineParsingResult Parse() // 如果选项不存在,则报告错误。 return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); } - AssignPropertyValue(optionMatch.PropertyName, optionMatch.PropertyIndex, [], v); + AssignOptionValue(optionMatch, v); break; } default: @@ -209,6 +209,40 @@ public CommandLineParsingResult Parse() return CommandLineParsingResult.Success; } + + private void AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) + { + SplitKeyValue(match.ValueType is OptionValueType.Dictionary, value, out var k, out var v); + AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + } + + private void AssignPositionalArgumentValue(PositionalArgumentValueMatch match, ReadOnlySpan value) + { + SplitKeyValue(false, value, out var k, out var v); + AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + } + + private static void SplitKeyValue(bool isDictionary, ReadOnlySpan item, + out ReadOnlySpan key, out ReadOnlySpan value) + { + if (!isDictionary) + { + key = []; + value = item; + return; + } + + // 截至目前,所有的字典类型都使用 key=value 形式,如果将来新增的风格有其他符号,我们再用一样的分隔符方式来配置。 + var index = item.IndexOf('='); + if (index < 0) + { + key = item; + value = []; + return; + } + key = item[..index]; + value = item[(index + 1)..]; + } } /// From 2dc1c1eb3b9e285336ad4b891adb67e9872286fd Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 13:53:56 +0800 Subject: [PATCH 008/193] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E4=B8=80=E4=BA=9B=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 104 +++++++------ .../Compiler/CommandLineParsingExtensions.cs | 41 +++++ .../Utils/Parsers/Callbacks.cs | 16 +- .../Utils/Parsers/CommandLineParser.cs | 143 ++++++++++++------ 4 files changed, 205 insertions(+), 99 deletions(-) create mode 100644 src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 9a44b5c3..dc55b4ce 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -16,6 +16,7 @@ public readonly record struct CommandLineParsingOptions SupportsLongOption = true, SupportsShortOption = true, SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.Both, OptionPrefix = CommandOptionPrefix.Any, @@ -33,6 +34,7 @@ public readonly record struct CommandLineParsingOptions SupportsLongOption = true, SupportsShortOption = true, SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = true, SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, @@ -50,6 +52,7 @@ public readonly record struct CommandLineParsingOptions SupportsLongOption = true, SupportsShortOption = true, SupportsShortOptionCombination = true, + SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = true, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, @@ -67,6 +70,7 @@ public readonly record struct CommandLineParsingOptions SupportsLongOption = false, SupportsShortOption = true, SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, NamingPolicy = CommandNamingPolicy.CamelCase, OptionPrefix = CommandOptionPrefix.SingleDash, @@ -84,8 +88,9 @@ public readonly record struct CommandLineParsingOptions SupportsLongOption = true, SupportsShortOption = true, SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = true, SupportsShortOptionValueWithoutSeparator = false, - NamingPolicy = CommandNamingPolicy.PascalCase, + NamingPolicy = CommandNamingPolicy.CamelCase, OptionPrefix = CommandOptionPrefix.Slash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), @@ -120,26 +125,20 @@ internal CommandLineStyleDetails(ushort magic) : this() public CommandNamingPolicy NamingPolicy { // [0] 表示是否额外编译时转换以支持 PascalCase/CamelCase 命名法 - // [1] 表示原样大小写,还是编译时按命名法转小写 - // [2] 表示是否同时支持 kebab-case 和 PascalCase/CamelCase 命名法 - get => _booleans[0, 1, 2] switch + // [1] 表示视选项上定义的命名法为 kebab-case,并允许用户使用此 kebab-case 命名法输入命令 + get => _booleans[0, 1] switch { - (true, true, false) => CommandNamingPolicy.PascalCase, - (true, false, false) => CommandNamingPolicy.CamelCase, - (false, true, false) => CommandNamingPolicy.KebabCase, - (false, false, false) => CommandNamingPolicy.KebabCaseLower, - (true, true, true) => CommandNamingPolicy.Both, - (true, false, true) => CommandNamingPolicy.BothLower, - _ => throw new InvalidOperationException("Invalid naming policy."), + (true, true) => CommandNamingPolicy.Both, + (true, false) => CommandNamingPolicy.KebabCase, + (false, true) => CommandNamingPolicy.CamelCase, + (false, false) => CommandNamingPolicy.Ordinal, }; - init => _booleans[0, 1, 2] = value switch + init => _booleans[0, 1] = value switch { - CommandNamingPolicy.PascalCase => (true, true, false), - CommandNamingPolicy.CamelCase => (true, false, false), - CommandNamingPolicy.KebabCase => (false, true, false), - CommandNamingPolicy.KebabCaseLower => (false, false, false), - CommandNamingPolicy.Both => (true, true, true), - CommandNamingPolicy.BothLower => (true, false, true), + CommandNamingPolicy.Both => (true, true), + CommandNamingPolicy.KebabCase => (true, false), + CommandNamingPolicy.CamelCase => (false, true), + CommandNamingPolicy.Ordinal => (false, false), _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), }; } @@ -149,15 +148,15 @@ public CommandNamingPolicy NamingPolicy /// public CommandOptionPrefix OptionPrefix { - // [3] 表示是否使用短横线(-)作为选项前缀 - // [4] 表示长选项是否使用双短横线(--) - get => _booleans[3, 4]switch + // [2] 表示是否使用短横线(-)作为选项前缀 + // [3] 表示长选项是否使用双短横线(--) + get => _booleans[2, 3]switch { (true, true) => CommandOptionPrefix.DoubleDash, (true, false) => CommandOptionPrefix.SingleDash, (false, _) => CommandOptionPrefix.Slash, }; - init => _booleans[3, 4] = value switch + init => _booleans[2, 3] = value switch { CommandOptionPrefix.DoubleDash => (true, true), CommandOptionPrefix.SingleDash => (true, false), @@ -171,8 +170,8 @@ public CommandOptionPrefix OptionPrefix /// public bool CaseSensitive { - get => _booleans[5]; - init => _booleans[5] = value; + get => _booleans[4]; + init => _booleans[4] = value; } /// @@ -180,8 +179,8 @@ public bool CaseSensitive /// public bool SupportsLongOption { - get => _booleans[6]; - init => _booleans[6] = value; + get => _booleans[5]; + init => _booleans[5] = value; } /// @@ -189,8 +188,8 @@ public bool SupportsLongOption /// public bool SupportsShortOption { - get => _booleans[7]; - init => _booleans[7] = value; + get => _booleans[6]; + init => _booleans[6] = value; } /// @@ -198,7 +197,23 @@ public bool SupportsShortOption /// 例如 -abc 等同于 -a -b -c。
/// 如果为 ,则 -abc 会被视为一个名为 "abc" 的短选项。 ///
+ /// + /// 此选项与 互斥。 + /// public bool SupportsShortOptionCombination + { + get => _booleans[7]; + init => _booleans[7] = value; + } + + /// + /// 当支持短选项时,是否支持多字符短选项名称。
+ /// 例如 -tl 作为 --terminal-logger 的短选项。 + ///
+ /// + /// 此选项与 互斥。 + /// + public bool SupportsMultiCharShortOption { get => _booleans[8]; init => _booleans[8] = value; @@ -239,40 +254,31 @@ public bool SupportsShortOptionValueWithoutSeparator /// /// 允许用户在命令行中使用的命令和选项的命名风格。 /// -/// -/// 虽然在不区分大小写时, 看起来是一样的,但在输出帮助文档时会以设定的为准。 -/// +[Flags] public enum CommandNamingPolicy : byte { /// - /// PascalCase 风格命名。 + /// 无视明明风格,属性上定义的字符串必须与用户输入的命令或选项名称完全匹配。 /// - PascalCase, + Ordinal = 0, /// - /// camelCase 风格命名。 + /// PascalCase/camelCase 风格命名。 /// - CamelCase, + CamelCase = 1, /// - /// kebab-case 风格命名,保持原样大小写。 + /// kebab-case 风格命名。 /// - KebabCase, - - /// - /// kebab-case 风格命名,且所有字母均为小写。 - /// - KebabCaseLower, - - /// - /// 以 kebab-case 命名风格为主,兼顾支持 PascalCase。 - /// - Both, + /// + /// 由于我们已经约定在定义属性时,属性已经用 kebab-case 命名风格标记了名字,所以此选项实际上含义与 是等同的。 + /// + KebabCase = 1 << 1, /// - /// 以 kebab-case 命名风格为主(所有字母均为小写),兼顾支持 PascalCase 和 camelCase。 + /// 以 kebab-case 命名风格为主,兼顾支持 PascalCase/camelCase。 /// - BothLower, + Both = CamelCase | KebabCase, } /// diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs b/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs new file mode 100644 index 00000000..08c0cee2 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs @@ -0,0 +1,41 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 提供一些扩展方法,辅助命令行解析器进行命令行解析。 +/// +public static class CommandLineParsingExtensions +{ + /// + /// 此命名风格是否支持 kebab-case 命名法。 + /// + /// 命名风格。 + /// 如果支持 kebab-case 命名法,则返回 ;否则返回 + /// + /// 由于我们已经约定在定义属性时,属性已经用 kebab-case 命名风格标记了名字,所以此选项实际上就是在判断是否使用定义的原样字符串。 + /// + public static bool SupportsOrdinal(this CommandNamingPolicy namingPolicy) + { + return namingPolicy switch + { + CommandNamingPolicy.KebabCase => true, + CommandNamingPolicy.Both => true, + CommandNamingPolicy.Ordinal => true, + _ => false, + }; + } + + /// + /// 此命名风格是否支持 PascalCase/camelCase 命名法。 + /// + /// 命名风格。 + /// 如果支持 PascalCase/camelCase 命名法,则返回 ;否则返回 + public static bool SupportsCamelCase(this CommandNamingPolicy namingPolicy) + { + return namingPolicy switch + { + CommandNamingPolicy.CamelCase => true, + CommandNamingPolicy.Both => true, + _ => false, + }; + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs index 68c693f0..ef1805d7 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/Callbacks.cs @@ -47,7 +47,13 @@ namespace DotNetCampus.Cli.Utils.Parsers; /// 此选项对应的属性名称。 /// 此选项对应的属性索引。 /// 此选项的值类型。 -public readonly record struct OptionValueMatch(string PropertyName, int PropertyIndex, OptionValueType ValueType); +public readonly record struct OptionValueMatch(string PropertyName, int PropertyIndex, OptionValueType ValueType) +{ + /// + /// 获取一个表示未匹配任何选项的匹配结果。 + /// + public static OptionValueMatch NotMatch => new("", -1, OptionValueType.NotExist); +} /// /// 源生成器匹配位置参数的匹配结果。 @@ -55,4 +61,10 @@ namespace DotNetCampus.Cli.Utils.Parsers; /// 此选项对应的属性名称。 /// 此选项对应的属性索引。 /// 此位置参数的值类型。 -public readonly record struct PositionalArgumentValueMatch(string PropertyName, int PropertyIndex, PositionalArgumentValueType ValueType); +public readonly record struct PositionalArgumentValueMatch(string PropertyName, int PropertyIndex, PositionalArgumentValueType ValueType) +{ + /// + /// 获取一个表示未匹配任何位置参数的匹配结果。 + /// + public static PositionalArgumentValueMatch NotMatch => new("", -1, PositionalArgumentValueType.NotExist); +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 04b71dfc..96ad48a8 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -31,6 +31,7 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int SupportsLongOption = Style.SupportsLongOption; SupportsShortOption = Style.SupportsShortOption; SupportsShortOptionCombination = Style.SupportsShortOptionCombination; + SupportsMultiCharShortOption = Style.SupportsMultiCharShortOption; SupportsShortOptionValueWithoutSeparator = Style.SupportsShortOptionValueWithoutSeparator; } @@ -51,6 +52,9 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int /// internal bool SupportsShortOptionCombination { get; } + /// + internal bool SupportsMultiCharShortOption { get; } + /// internal bool SupportsShortOptionValueWithoutSeparator { get; } @@ -87,7 +91,7 @@ public CommandLineParsingResult Parse() { var arguments = _commandLine.CommandLineArguments; var currentOptionName = new OptionName(false, []); - var currentOption = new OptionValueMatch("", -1, OptionValueType.Normal); + var currentOption = OptionValueMatch.NotMatch; var currentPositionArgumentIndex = 0; var lastState = Cat.Start; @@ -162,7 +166,19 @@ public CommandLineParsingResult Parse() } case Cat.MultiShortOptions: { - // 逐个处理多个短选项。 + // 如果支持多字符短选项,则优先作为多字符短选项处理。 + if (SupportsMultiCharShortOption) + { + var m = optionName.Name; + var optionMatch = MatchShortOption(m, _caseSensitive); + if (optionMatch.ValueType is not OptionValueType.NotExist) + { + // 是一个多字符短选项。 + AssignOptionValue(optionMatch, []); + break; + } + } + // 随后,尝试逐个处理多个短选项。 for (var i = 0; i < optionName.Name.Length; i++) { var n = optionName.Name[i..(i + 1)]; @@ -178,32 +194,33 @@ public CommandLineParsingResult Parse() } case Cat.MultiShortOptionsOrShortOptionConcatWithValue: { - // 先看看是否是一个多字符短选项,如果不是,再看看是否是单个字符无分隔符带值的短选项。 - var m = optionName.Name; - var optionMatch = MatchShortOption(m, _caseSensitive); - if (optionMatch.ValueType is not OptionValueType.NotExist) + var name = optionName.Name; + // 如果支持多字符短选项,则优先作为多字符短选项处理。 + if (SupportsMultiCharShortOption) { - // 是一个多字符短选项。 - AssignOptionValue(optionMatch, []); - break; + var optionMatch = MatchShortOption(name, _caseSensitive); + if (optionMatch.ValueType is not OptionValueType.NotExist) + { + // 是一个多字符短选项。 + AssignOptionValue(optionMatch, []); + break; + } } - // 不是一个多字符短选项,尝试解析为单个字符无分隔符带值的短选项。 - var n = m[..1]; - var v = m[1..]; - optionMatch = MatchShortOption(n, _caseSensitive); - if (optionMatch.ValueType is OptionValueType.NotExist) + // 随后,尝试解析为单个字符无分隔符带值的短选项。 { - // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); + var o = name[..1]; + var v = name[1..]; + var optionMatch = MatchShortOption(o, _caseSensitive); + if (optionMatch.ValueType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, o); + } + AssignOptionValue(optionMatch, v); } - AssignOptionValue(optionMatch, v); - break; - } - default: - { - // 其他状态要么已经处理过了,要不还未处理,要么不需要处理,所以不需要做任何事情。 break; } + // 其他状态要么已经处理过了,要不还未处理,要么不需要处理,所以不需要做任何事情。 } } @@ -373,10 +390,26 @@ private bool ParsePostPositionalArgumentRegion() private bool ParseOptionOrPositionalArgument() { var argument = _argument.AsSpan(); - if (argument.Length is 0 or 1) + if (argument.Length is 0) + { + // 空字符串,视为位置参数。 + Type = Cat.PositionalArgument; + Value = argument; + return true; + } + if (argument.Length is 1) { - // TODO 针对单独有选项分隔符的,要报错。 - // 空参数或单个字符(无法组成选项),视为位置参数。 + // 单个字符,确定一下是否是选项分隔符,如果是则要报错。 + Span separators = stackalloc char[4]; + _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + if (argument.IndexOfAny(separators) >= 0) + { + // 仅包含分隔符,视为错误选项。 + Type = Cat.ErrorOption; + return true; + } + // 单个字符(无法组成选项),视为位置参数。 Type = Cat.PositionalArgument; Value = argument; return true; @@ -443,9 +476,6 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; - var supportsCombination = _parser.SupportsShortOptionCombination; - var supportsNoSeparator = _parser.SupportsShortOptionValueWithoutSeparator; - var index = argument.IndexOfAny(separators); if (index is 0) { @@ -453,6 +483,13 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) Type = Cat.ErrorOption; return true; } + if (argument.Length is 1) + { + // 单独的短选项。 + Type = Cat.ShortOption; + Option = new OptionName(false, argument); + return true; + } if (index > 0) { // 带值的短选项。 @@ -461,28 +498,38 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) Value = argument[(index + 1)..]; return true; } - if (argument.Length is 1 || !supportsCombination) - { - // 单独的短选项。 - Type = Cat.ShortOption; - Option = new OptionName(false, argument); - return true; - } - // TODO 不可能三种都存在 - // -abc -a -b -c - // -abc - // -abc -a bc - if (supportsNoSeparator) + + // 对于不带值的短选项,存在以下三种情况: + // 1. -abc 表示 -a -b -c 三个布尔短选项。 + // 2. -abc 表示 -a 选项的值为 bc。 + // 3. -abc 表示一个名为 abc 的多字符短选项。 + // 目前不存在任何一种命令行风格同时支持上述三种情况,所以我们可以消除一些不确定性。 + var supportsCombination = _parser.SupportsShortOptionCombination; + var supportsNoSeparator = _parser.SupportsShortOptionValueWithoutSeparator; + switch (supportsCombination, supportsNoSeparator) { - // 不确定是多个短选项,还是一个无分隔符的带值短选项。 - Type = Cat.MultiShortOptionsOrShortOptionConcatWithValue; - Option = new OptionName(false, argument); - return true; + // 支持短选项组合,也支持无分隔符带值的短选项。(上述 1 和 2,从实际考虑消除了 3) + case (true, true): + Type = Cat.MultiShortOptionsOrShortOptionConcatWithValue; + Option = new OptionName(false, argument); + return true; + case (true, false): + // 支持短选项组合,不支持无分隔符带值的短选项。(上述 1 和 3) + Type = Cat.MultiShortOptions; + Option = new OptionName(false, argument); + return true; + case (false, true): + // 不支持短选项组合,但支持无分隔符带值的短选项。(上述 2,从实际考虑消除了 3) + Type = Cat.ShortOptionWithValue; + Option = new OptionName(false, argument[..1]); + Value = argument[1..]; + return true; + case (false, false): + // 既不支持短选项组合,也不支持无分隔符带值的短选项。(上述 3) + Type = Cat.ShortOption; + Option = new OptionName(false, argument); + return true; } - // 多个短选项。 - Type = Cat.MultiShortOptions; - Option = new OptionName(false, argument); - return true; } private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) From 2c4452a0b432a9dc6bbe7fc161602438a003be39 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 13:55:14 +0800 Subject: [PATCH 009/193] =?UTF-8?q?=E7=94=B1=E4=BA=8E=E6=88=91=E4=BB=AC?= =?UTF-8?q?=E6=9C=89=E4=BA=86=20handler=20=E6=9D=A5=E8=B5=8B=E5=80=BC?= =?UTF-8?q?=EF=BC=8C=E6=89=80=E4=BB=A5=E4=B8=8D=E5=86=8D=E9=9C=80=E8=A6=81?= =?UTF-8?q?=20optionName=20=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 96ad48a8..ba301cf5 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -90,7 +90,6 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int public CommandLineParsingResult Parse() { var arguments = _commandLine.CommandLineArguments; - var currentOptionName = new OptionName(false, []); var currentOption = OptionValueMatch.NotMatch; var currentPositionArgumentIndex = 0; var lastState = Cat.Start; @@ -111,7 +110,6 @@ public CommandLineParsingResult Parse() case Cat.LongOption or Cat.ShortOption or Cat.Option: { // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 - currentOptionName = optionName; var optionMatch = state switch { Cat.LongOption => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), @@ -137,7 +135,6 @@ public CommandLineParsingResult Parse() { // 如果不是集合,那么此选项已经结束。 // 清空上一个选项,避免误用。 - currentOptionName = new OptionName(false, []); currentOption = DefaultOptionValueHandler; } break; From c35ebe18e4baf957e6f59b587c219e2f5ce24325 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 14:00:17 +0800 Subject: [PATCH 010/193] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E5=AD=97=E7=AC=A6=E7=9F=AD=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index ba301cf5..47a7fc25 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -489,10 +489,16 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) } if (index > 0) { - // 带值的短选项。 - Type = Cat.ShortOptionWithValue; - Option = new OptionName(false, argument[..index]); - Value = argument[(index + 1)..]; + if (index is 1 || _parser.SupportsMultiCharShortOption) + { + // 带值的短选项。 + Type = Cat.ShortOptionWithValue; + Option = new OptionName(false, argument[..index]); + Value = argument[(index + 1)..]; + return true; + } + // 分隔符出现在第二个字符之后,但不支持多字符短选项,报告错误。 + Type = Cat.ErrorOption; return true; } From 29b2e3f0beece4db46151399b627d332ea7ce739 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 15:29:31 +0800 Subject: [PATCH 011/193] =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=97=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 47a7fc25..3568230f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -226,26 +226,73 @@ public CommandLineParsingResult Parse() private void AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) { - SplitKeyValue(match.ValueType is OptionValueType.Dictionary, value, out var k, out var v); - AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + if (match.ValueType is OptionValueType.Collection) + { + Span separators = stackalloc char[4]; + Style.CollectionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + + var start = 0; + while (start < value.Length) + { + var index = value[start..].IndexOfAny(separators); + if (index < 0) + { + // 剩余部分没有分隔符,全部作为一个值。 + AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value[start..]); + break; + } + if (index > 0) + { + // 截取分隔符前的部分作为一个值。 + AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value.Slice(start, index)); + } + // 跳过分隔符,继续处理后续部分。 + start += index + 1; + } + } + else if (match.ValueType is OptionValueType.Dictionary) + { + Span separators = stackalloc char[4]; + Style.CollectionValueSeparators.CopyTo(separators, out var length); + separators = separators[..length]; + + var start = 0; + while (start < value.Length) + { + var index = value[start..].IndexOfAny(separators); + if (index < 0) + { + // 剩余部分没有分隔符,全部作为一个值。 + SplitKeyValue(value[start..], out var k, out var v); + AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + break; + } + if (index > 0) + { + // 截取分隔符前的部分作为一个值。 + SplitKeyValue(value.Slice(start, index), out var k, out var v); + AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + } + // 跳过分隔符,继续处理后续部分。 + start += index + 1; + } + } + else + { + // 普通值。 + AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value); + } } private void AssignPositionalArgumentValue(PositionalArgumentValueMatch match, ReadOnlySpan value) { - SplitKeyValue(false, value, out var k, out var v); - AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); + AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value); } - private static void SplitKeyValue(bool isDictionary, ReadOnlySpan item, + private static void SplitKeyValue(ReadOnlySpan item, out ReadOnlySpan key, out ReadOnlySpan value) { - if (!isDictionary) - { - key = []; - value = item; - return; - } - // 截至目前,所有的字典类型都使用 key=value 形式,如果将来新增的风格有其他符号,我们再用一样的分隔符方式来配置。 var index = item.IndexOf('='); if (index < 0) @@ -577,7 +624,7 @@ private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() argument is "")) { Type = Cat.OptionValue; - Value = "true".AsSpan(); + Value = "1".AsSpan(); return true; } if (argument.Length is > 0 and <= 5 && ( @@ -587,7 +634,7 @@ private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() argument.Equals("0", StringComparison.OrdinalIgnoreCase))) { Type = Cat.OptionValue; - Value = "false".AsSpan(); + Value = "0".AsSpan(); return true; } return ParseOptionOrPositionalArgument(); From f61a3687be02a94baac2918a52faf517af277dde Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 15:31:07 +0800 Subject: [PATCH 012/193] =?UTF-8?q?=E7=BB=99=E6=BA=90=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E7=BC=96=E5=86=99=E7=94=A8=E6=9D=A5=E5=81=9A=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=B5=8B=E5=80=BC=E7=9A=84=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/PropertyAssignments.cs | 325 +++++++++++++++++- 1 file changed, 307 insertions(+), 18 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index 935c8f67..c04be8df 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -1,55 +1,311 @@ -namespace DotNetCampus.Cli.Compiler; - -/// -/// 为源生成器解析命令行提供属性赋值辅助。 -/// -/// -/// 当然,这个接口只为了给所有的实现提供实现标准。
-/// 由于所有的实现都是结构,所以不会有任何代码直接使用到这个接口。 -///
-public interface IPropertyAssignment -{ +#if NETCOREAPP3_1_OR_GREATER +using System.Collections.Immutable; +#endif +using System.Collections.ObjectModel; -} +namespace DotNetCampus.Cli.Compiler; /// /// 专门解析来自命令行的布尔类型,并辅助赋值给属性。 /// -public readonly struct BooleanPropertyAssignment +public struct BooleanArgument { + /// + /// 存储解析到的布尔值。 + /// + private bool? _value; + + /// + /// 当命令行直接或间接输入了一个布尔参数时,调用此方法赋值。 + /// + /// 解析到的布尔值。 + public void Assign(bool value) + { + _value = value; + } + /// + /// 将解析到的值转换为布尔值。 + /// + public bool? ToBoolean() + { + return _value; + } } /// /// 专门解析来自命令行的数值类型,并辅助赋值给属性。 /// -public readonly struct NumberPropertyAssignment +public struct NumberArgument { + /// + /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 + /// + public bool IgnoreExceptions { get; init; } + + /// + /// 存储解析到的数值。 + /// + private decimal? _value; + + /// + /// 当命令行输入了一个数值参数时,调用此方法赋值。 + /// + /// 解析到的数值字符串。 + public void Assign(ReadOnlySpan value) + { + if (decimal.TryParse(value +#if !NETCOREAPP3_1_OR_GREATER + .ToString() +#endif + , out var doubleValue)) + { + _value = doubleValue; + } + else if (!IgnoreExceptions) + { + throw new FormatException($"无法将 \"{value.ToString()}\" 转换为数值。"); + } + } + + /// + /// 将解析到的值转换为字节。 + /// + public byte? ToByte() => (byte?)_value; + + /// + /// 将解析到的值转换为有符号字节。 + /// + public sbyte? ToSByte() => (sbyte?)_value; + + /// + /// 将解析到的值转换为高精度浮点数。 + /// + public decimal? ToDecimal() => _value; + + /// + /// 将解析到的值转换为双精度浮点数。 + /// + public double? ToDouble() => (double?)_value; + + /// + /// 将解析到的值转换为单精度浮点数。 + /// + public float? ToSingle() => (float?)_value; + + /// + /// 将解析到的值转换为 32 位整数。 + /// + public int? ToInt32() => (int?)_value; + + /// + /// 将解析到的值转换为无符号 32 位整数。 + /// + public uint? ToUInt32() => (uint?)_value; + + /// + /// 将解析到的值转换为指针大小的整数。 + /// + public nint? ToIntPtr() => (nint?)_value; + /// + /// 将解析到的值转换为无符号指针大小的整数。 + /// + public nuint? ToUIntPtr() => (nuint?)_value; + + /// + /// 将解析到的值转换为 64 位整数。 + /// + public long? ToInt64() => (long?)_value; + + /// + /// 将解析到的值转换为无符号 64 位整数。 + /// + public ulong? ToUInt64() => (ulong?)_value; + + /// + /// 将解析到的值转换为 16 位整数。 + /// + public short? ToInt16() => (short?)_value; + + /// + /// 将解析到的值转换为无符号 16 位整数。 + /// + public ushort? ToUInt16() => (ushort?)_value; } /// /// 专门解析来自命令行的字符串类型,并辅助赋值给属性。 /// -public readonly struct StringPropertyAssignment +public struct StringArgument { + /// + /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 + /// + public bool IgnoreExceptions { get; init; } + + /// + /// 存储解析到的字符串值。 + /// + private string? _text; + /// + /// 当命令行输入了一个字符串参数时,调用此方法赋值。 + /// + /// 解析到的字符串值。 + public void Assign(ReadOnlySpan value) + { + _text = value.ToString(); + } + + /// + /// 将解析到的值转换为字符。 + /// + /// 如果字符串长度为 1,则返回该字符;否则返回 null。 + public char? ToChar() => _text switch + { + null => null, + { Length: 1 } => _text[0], + _ when IgnoreExceptions => null, + _ => throw new FormatException($"无法将 \"{_text}\" 转换为字符,因为它的长度不为 1。"), + }; + + /// + /// 将解析到的值转换为字符串。 + /// + public override string? ToString() + { + return _text; + } } /// /// 专门解析来自命令行的字符串集合类型,并辅助赋值给属性。 /// -public readonly struct StringsPropertyAssignment +public struct StringListArgument { + /// + /// 存储解析到的字符串列表。 + /// + private List? _list; + + /// + /// 当命令行输入了一个字符串参数时,调用此方法追加值。 + /// + /// 解析到的字符串值。 + public void Append(ReadOnlySpan value) + { + _list ??= []; + _list.Add(value.ToString()); + } + + /// + /// 将解析到的值转换为字符串数组。 + /// + public string[] ToArray() => _list switch + { + null or { Count: 0 } => [], + { } values => [..values], + }; + +#if NETCOREAPP3_1_OR_GREATER + /// + /// 将解析到的值转换为不可变数组。 + /// + public ImmutableArray ToImmutableArray() => _list switch + { +#if NET8_0_OR_GREATER + null or { Count: 0 } => [], + { } values => [..values], +#else + null or { Count: 0 } => ImmutableArray.Empty, + { } values => values.ToImmutableArray(), +#endif + }; + /// + /// 将解析到的值转换为不可变哈希集合。 + /// + public ImmutableHashSet ToImmutableHashSet() => _list switch + { +#if NET8_0_OR_GREATER + null or { Count: 0 } => [], + { } values => [..values], +#else + null or { Count: 0 } => ImmutableHashSet.Empty, + { } values => values.ToImmutableHashSet(), +#endif + }; + +#endif + + /// + /// 将解析到的值转换为集合。 + /// + public Collection ToCollection() => _list switch + { + null or { Count: 0 } => [], + { } values => [..values], + }; + + /// + /// 将解析到的值转换为列表。 + /// + public List ToList() => _list switch + { + null or { Count: 0 } => [], + { } values => values, + }; } /// /// 专门解析来自命令行的字典类型,并辅助赋值给属性。 /// -public readonly struct DictionaryPropertyAssignment +public struct DictionaryArgument { + /// + /// 存储解析到的字符串字典。 + /// + private Dictionary _dictionary; + + /// + /// 当命令行输入了一个键值对参数时,调用此方法追加值。 + /// + /// 解析到的键。 + /// 解析到的值。 + public void Append(ReadOnlySpan key, ReadOnlySpan value) + { + _dictionary ??= []; + _dictionary[key.ToString()] = value.ToString(); + } + + /// + /// 将解析到的值转换为键值对。 + /// + public KeyValuePair? ToKeyValuePair() + { + if (_dictionary is null || _dictionary.Count == 0) + { + return null; + } + if (_dictionary.Count > 1) + { + throw new InvalidOperationException("字典包含多个元素,无法转换为 KeyValuePair。"); + } + + using var enumerator = _dictionary.GetEnumerator(); + enumerator.MoveNext(); + return enumerator.Current; + } + + /// + /// 将解析到的值转换为字典。 + /// + public Dictionary ToDictionary() + { + return _dictionary ?? []; + } } /// @@ -59,7 +315,40 @@ public readonly struct DictionaryPropertyAssignment /// 源生成器会为各个枚举生成专门的编译时类型来处理枚举的赋值。
/// 此类型是为那些在运行时才知道枚举类型的场景准备的。 /// -public readonly struct RuntimeEnumPropertyAssignment +public struct RuntimeEnumArgument where T : unmanaged, Enum { + /// + /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 + /// + public bool IgnoreExceptions { get; init; } + + /// + /// 存储解析到的枚举值。 + /// + private T? _value; + + /// + /// 当命令行输入了一个数值参数时,调用此方法赋值。 + /// + /// 解析到的数值字符串。 + public void Assign(ReadOnlySpan value) + { + if (Enum.TryParse(value +#if !NET6_0_OR_GREATER + .ToString() +#endif + , ignoreCase: true, out var enumValue)) + { + _value = enumValue; + } + else if (!IgnoreExceptions) + { + throw new FormatException($"无法将 \"{value.ToString()}\" 转换为 {typeof(T).FullName} 枚举。"); + } + } + /// + /// 将解析到的值转换为枚举。 + /// + public T? ToEnum() => _value; } From fdcd0cc3d6296a167536adffc292f89448476b97 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 15:31:27 +0800 Subject: [PATCH 013/193] =?UTF-8?q?=E8=AF=95=E4=B8=80=E4=B8=8B=E5=88=9D?= =?UTF-8?q?=E7=89=88=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=88=E6=89=8B?= =?UTF-8?q?=E5=86=99=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExperimentalTests.cs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs b/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs new file mode 100644 index 00000000..48fc5ad6 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs @@ -0,0 +1,169 @@ +#nullable enable +using System; +using System.Collections.Generic; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Utils.Parsers; + +namespace DotNetCampus.Cli.Tests; + +internal class ExperimentalTests +{ + public void Test() + { + var foo = CommandLine.Parse( + ["--bool", "--number:42", "--string", "hello", "--strings", "one", "two", "three", "--dict", "key1=value"], + CommandLineParsingOptions.DotNet) + .As(); + } +} + +internal record Foo +{ + [Option("bool")] + public required bool BooleanProperty { get; init; } + + [Option("number")] + public required int NumberProperty { get; init; } + + [Option("string")] + public required string StringProperty { get; init; } + + [Option("strings")] + public required IReadOnlyList StringsProperty { get; init; } + + [Option("dict")] + public required IReadOnlyDictionary DictionaryProperty { get; init; } + + [Option("log-level")] + public required LogLevel LogLevelProperty { get; init; } +} + +internal sealed class ExperimentalFooBuilder(CommandLine commandLine) +{ + private BooleanArgument BooleanProperty { get; } + private NumberArgument NumberProperty { get; } + private StringArgument StringProperty { get; } + private StringListArgument StringsProperty { get; } + private DictionaryArgument DictionaryProperty { get; } + private __GeneratedEnumPropertyAssignment__LogLevel__ LogLevelProperty { get; } + + public Foo Build() + { + var parser = new CommandLineParser(commandLine, "Foo", 0) + { + MatchLongOption = MatchLongOption, + MatchShortOption = MatchShortOption, + MatchPositionalArguments = MatchPositionalArguments, + AssignPropertyValue = AssignPropertyValue, + }; + parser.Parse(); + return BuildCore(); + } + + private OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy) + { + // 先原样匹配一遍。 + if (namingPolicy.SupportsOrdinal()) + { + var match = longOption switch + { + "boolean-property" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), + _ => OptionValueMatch.NotMatch, + }; + if (match != OptionValueMatch.NotMatch) + { + return match; + } + } + // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 + if (namingPolicy.SupportsCamelCase()) + { + var match = longOption switch + { + "boolean-property" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), + _ => OptionValueMatch.NotMatch, + }; + return match; + } + return OptionValueMatch.NotMatch; + } + + private OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) + { + var match = shortOption switch + { + "b" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), + _ => OptionValueMatch.NotMatch, + }; + return match; + } + + private PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) + { + if (argumentIndex is 0) + { + return new PositionalArgumentValueMatch(nameof(StringProperty), 2, PositionalArgumentValueType.Normal); + } + return PositionalArgumentValueMatch.NotMatch; + } + + private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) + { + _ = propertyIndex switch + { + 0 => BooleanProperty.Assign(value[0] == '1'), + 1 => NumberProperty.Assign(value), + 2 => StringProperty.Assign(value), + 3 => StringsProperty.Append(value), + 4 => DictionaryProperty.Append(key, value), + 5 => LogLevelProperty.SetValue(value), + _ => throw new ArgumentOutOfRangeException(nameof(propertyIndex), propertyIndex, null), + }; + } + + private Foo BuildCore() + { + var result = new Foo + { + BooleanProperty = BooleanProperty.ToBoolean() ?? throw new InvalidOperationException("BooleanProperty 未被赋值"), + NumberProperty = NumberProperty.ToInt32() ?? throw new InvalidOperationException("NumberProperty 未被赋值"), + StringProperty = StringProperty.ToString() ?? throw new InvalidOperationException("StringProperty 未被赋值"), + StringsProperty = StringsProperty.ToList() ?? throw new InvalidOperationException("StringsProperty 未被赋值"), + DictionaryProperty = DictionaryProperty.ToDictionary() ?? throw new InvalidOperationException("DictionaryProperty 未被赋值"), + LogLevelProperty = LogLevelProperty.ToEnum() ?? throw new InvalidOperationException("LogLevelProperty 未被赋值"), + }; + + // 1. [RawArguments] + // result.MainArgs = commandLine.CommandLineArguments; + + // 2. [Option] + // There is no option to be assigned. + + // 3. [Value] + // There is no positional argument to be assigned. + + return result; + } + + // ReSharper disable once InconsistentNaming + private struct __GeneratedEnumPropertyAssignment__LogLevel__ + { + private LogLevel? _value; + + public bool SetValue(ReadOnlySpan value) + { + _ = value switch + { + "0" or "Debug" or "debug" => _value = LogLevel.Debug, + "1" or "Info" or "info" => _value = LogLevel.Info, + "2" or "Warning" or "warning" => _value = LogLevel.Warning, + "3" or "Error" or "error" => _value = LogLevel.Error, + "4" or "Fatal" or "fatal" => _value = LogLevel.Critical, + _ => throw new ArgumentOutOfRangeException(nameof(value), value.ToString(), null), + }; + return true; + } + + public LogLevel? ToEnum() => _value; + } +} From aa29eb08fd137c826d0e6a28640df212bd51353a Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 16:01:14 +0800 Subject: [PATCH 014/193] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A0=E5=AD=90?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E4=B9=9F=E8=83=BD=E6=A0=87=E8=AE=B0=20CommandAttribut?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelProviding/CommandModelProvider.cs | 2 +- src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 0e0aa34a..907d6902 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -71,7 +71,7 @@ public static IncrementalValuesProvider SelectComm } var @namespace = typeSymbol.ContainingNamespace.ToDisplayString(); - var commandNames = attribute?.ConstructorArguments[0].Value?.ToString(); + var commandNames = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString(); var isPublic = typeSymbol.DeclaredAccessibility == Accessibility.Public; return new CommandObjectGeneratingModel diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs index d9f6a4aa..0bf8cfbe 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs @@ -24,7 +24,7 @@ /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public sealed class CommandAttribute(string? names) : CommandLineAttribute +public sealed class CommandAttribute(string? names = null) : CommandLineAttribute { /// /// 获取命令行的命令,可以是单个词组的主命令(Main Command),也可以是多个词组的子命令或多级子命令(Sub Command)。 From 9412fbf6038679ee20da2723de83516a5affed16 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 18:51:17 +0800 Subject: [PATCH 015/193] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E8=A7=A3=E6=9E=90=E5=99=A8=E7=9A=84=E6=BA=90?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 1 + .../DotNetCampus.CommandLine.Analyzer.csproj | 1 + .../GeneratorInfo.cs | 68 -- .../Generators/BuilderGenerator.cs | 690 +++++++++--------- .../Generators/ModelBuilderGenerator.cs | 212 ++++++ .../ModelProviding/CommandModelProvider.cs | 311 ++++++-- .../CommandLineParsingOptions.cs | 14 +- .../Compiler/CommandLineParsingExtensions.cs | 4 +- .../Compiler/OptionAttribute.cs | 113 +-- .../Compiler/PropertyAssignments.cs | 2 +- .../Utils/Parsers/CommandLineParser.cs | 6 +- 11 files changed, 870 insertions(+), 552 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine.Analyzer/GeneratorInfo.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ee7ca77..7800d8c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ + diff --git a/src/DotNetCampus.CommandLine.Analyzer/DotNetCampus.CommandLine.Analyzer.csproj b/src/DotNetCampus.CommandLine.Analyzer/DotNetCampus.CommandLine.Analyzer.csproj index 2495d2f5..30fe79b5 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/DotNetCampus.CommandLine.Analyzer.csproj +++ b/src/DotNetCampus.CommandLine.Analyzer/DotNetCampus.CommandLine.Analyzer.csproj @@ -8,6 +8,7 @@ + all diff --git a/src/DotNetCampus.CommandLine.Analyzer/GeneratorInfo.cs b/src/DotNetCampus.CommandLine.Analyzer/GeneratorInfo.cs deleted file mode 100644 index 6fead507..00000000 --- a/src/DotNetCampus.CommandLine.Analyzer/GeneratorInfo.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Reflection; -using Microsoft.CodeAnalysis; - -namespace DotNetCampus.CommandLine; - -internal static class GeneratorInfo -{ - public static string RootNamespace => typeof(GeneratorInfo).Namespace!; - - public static string ToolName { get; } = typeof(GeneratorInfo).Assembly - .GetCustomAttribute()?.Title ?? typeof(GeneratorInfo).Namespace!; - - public static string ToolVersion { get; } = typeof(GeneratorInfo).Assembly - .GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; - - private static readonly SymbolDisplayFormat GlobalDisplayFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static readonly SymbolDisplayFormat NotNullGlobalDisplayFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - private static readonly SymbolDisplayFormat GlobalTypeOfDisplayFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.None, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - public static string ToGlobalDisplayString(this ISymbol symbol) - { - return symbol.ToDisplayString(GlobalDisplayFormat); - } - - public static string ToNotNullGlobalDisplayString(this ISymbol symbol) - { - // 对于 Nullable(例如 Nullable、int?)等,是类型而不是可空标记,所以需要特别取出里面的类型 T。 - if (symbol is ITypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) - { - return typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType - // 获取 Nullable 中的 T。 - ? namedType.TypeArguments[0].ToDisplayString(GlobalDisplayFormat) - // 处理直接带有可空标记的类型 (int? 这种形式)。 - : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(GlobalDisplayFormat); - } - - // 对于其他符号或非可空类型,使用不包含可空引用类型修饰符的格式 - return symbol.ToDisplayString(NotNullGlobalDisplayFormat); - } - - public static string ToGlobalTypeOfDisplayString(this INamedTypeSymbol symbol) - { - var name = symbol.ToDisplayString(GlobalTypeOfDisplayFormat); - return symbol.IsGenericType ? $"{name}<{new string(',', symbol.TypeArguments.Length - 1)}>" : name; - } -} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs index 48bd6b4e..bfc53e98 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs @@ -1,345 +1,345 @@ -using System.Collections.Immutable; -using DotNetCampus.CommandLine.Generators.ModelProviding; -using DotNetCampus.CommandLine.Utils.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DotNetCampus.CommandLine.Generators; - -[Generator(LanguageNames.CSharp)] -public class BuilderGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var analyzerConfigOptionsProvider = context.AnalyzerConfigOptionsProvider; - var commandOptionsProvider = context.SelectCommandObjects(); - var assemblyCommandsProvider = context.SelectAssemblyCommands(); - - context.RegisterSourceOutput( - commandOptionsProvider, - Execute); - - context.RegisterSourceOutput( - assemblyCommandsProvider.Collect().Combine(commandOptionsProvider.Collect()).Combine(analyzerConfigOptionsProvider), - Execute); - } - - private void Execute(SourceProductionContext context, CommandObjectGeneratingModel model) - { - var code = GenerateCommandObjectCreatorCode(model); - context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.cs", code); - } - - private void Execute(SourceProductionContext context, - ((ImmutableArray Left, ImmutableArray Right) Left, AnalyzerConfigOptionsProvider Right) - args) - { - var ((assemblyCommandsGeneratingModels, commandOptionsGeneratingModels), analyzerConfigOptions) = args; - commandOptionsGeneratingModels = [..commandOptionsGeneratingModels.OrderBy(x => x.GetBuilderTypeName())]; - - if (analyzerConfigOptions.GlobalOptions.TryGetValue("DotNetCampusCommandLineUseInterceptor", out var useInterceptor) - && !useInterceptor) - { - var moduleInitializerCode = GenerateModuleInitializerCode(commandOptionsGeneratingModels); - context.AddSource("CommandLine.Metadata/_ModuleInitializer.g.cs", moduleInitializerCode); - } - - foreach (var assemblyCommandsGeneratingModel in assemblyCommandsGeneratingModels) - { - var code = GenerateAssemblyCommandHandlerCode(assemblyCommandsGeneratingModel, commandOptionsGeneratingModels); - context.AddSource( - $"CommandLine.Metadata/{assemblyCommandsGeneratingModel.Namespace}.{assemblyCommandsGeneratingModel.AssemblyCommandHandlerType.Name}.g.cs", - code); - } - } - - private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) - { - // | required | nullable | cli | 行为 | - // | -------- | -------- | --- | ---------- | - // | 0 | 0 | 0 | 分析器警告 | - // | 1 | 0 | 0 | 抛异常 | - // | 0 | 1 | 0 | 默认值 | - // | 1 | 1 | 0 | 抛异常 | - // | 0 | 0 | 1 | 赋值 | - // | 1 | 0 | 1 | 赋值 | - // | 0 | 1 | 1 | 赋值 | - // | 1 | 1 | 1 | 赋值 | - - var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); - var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); - var initValueProperties = model.ValueProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); - var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); - var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); - var setValueProperties = model.ValueProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); - return $$""" -#nullable enable -namespace {{model.Namespace}}; - -/// -/// 辅助 生成命令行选项、子命令或处理函数的创建。 -/// -{{(model.IsPublic ? "public" : "internal")}} sealed class {{model.GetBuilderTypeName()}} -{ - public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) - { - var caseSensitive = commandLine.DefaultCaseSensitive; - var result = new {{model.CommandObjectType.ToGlobalDisplayString()}} - { - // 1. [RawArguments] -{{(initRawArgumentsProperties.Length is 0 ? " // MainArgs = commandLine.CommandLineArguments," : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} - - // 2. [Option] -{{(initOptionProperties.Length is 0 ? " // There is no option to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateOptionPropertyAssignment)))}} - - // 3. [Value] -{{(initValueProperties.Length is 0 ? " // There is no positional argument to be initialized." : string.Join("\n", initValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} - }; - - // 1. [RawArguments] -{{(setRawArgumentsProperties.Length is 0 ? " // result.MainArgs = commandLine.CommandLineArguments;" : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} - - // 2. [Option] -{{(setOptionProperties.Length is 0 ? " // There is no option to be assigned." : string.Join("\n", setOptionProperties.Select(GenerateOptionPropertyAssignment)))}} - - // 3. [Value] -{{(setValueProperties.Length is 0 ? " // There is no positional argument to be assigned." : string.Join("\n", setValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} - - return result; - } -} - -"""; - } - - private string GenerateOptionPropertyAssignment(OptionPropertyGeneratingModel property, int modelIndex) - { - var isInitProperty = property.IsRequired || property.IsInitOnly; - var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; - var caseSensitive = property.CaseSensitive switch - { - true => ", true", - false => ", false", - null => "", - }; - var exception = property.IsRequired - ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{property.GetDisplayCommandOption()}'. Command line: {{commandLine}}\", \"{property.PropertyName}\")" - : (property.IsNullable, property.IsValueType) switch - { - (true, _) => "null", - (false, true) => "default", - (false, false) => "null!", - }; - - var getters = property.GenerateAllNames( - shortOption => $"""commandLine.GetShortOption("{shortOption}"{caseSensitive})""", - longOption => $"""commandLine.GetOption("{longOption}"{caseSensitive})""", - (caseSensitiveLongOption, ignoreCaseLongName) => - $"""commandLine.GetOption(caseSensitive ? "{caseSensitiveLongOption}" : "{ignoreCaseLongName}"{caseSensitive})""", - aliasOption => $"""commandLine.GetOption("{aliasOption}")""" - ); - - return (isInitProperty, getters) switch - { - // [Option("OptionName")] - // public required string PropertyName { get; init; } - (true, { Count: 1 }) => $""" - {property.PropertyName} = {getters[0]}{toMethod} ?? {exception}, -""", - // [Option('o', "OptionName")] - // public required string PropertyName { get; init; } - (true, _) => $""" - {property.PropertyName} = ({string.Join("\n ?? ", getters)}){toMethod} - ?? {exception}, -""", - // [Option("OptionName")] - // public string PropertyName { get; set; } - (false, { Count: 1 }) => $$""" - if ({{getters[0]}}{{toMethod}} is { } o{{modelIndex}}) - { - result.{{property.PropertyName}} = o{{modelIndex}}; - } -""", - // [Option('o', "OptionName")] - // public string PropertyName { get; set; } - (false, _) => $$""" - if (({{string.Join("\n ?? ", getters)}}){{toMethod}} is { } o{{modelIndex}}) - { - result.{{property.PropertyName}} = o{{modelIndex}}; - } -""", - }; - } - - private string GenerateValuePropertyAssignment(CommandObjectGeneratingModel model, ValuePropertyGeneratingModel property, int modelIndex) - { - var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; - var baseIndex = model.GetCommandLevel(); - var indexLengthCode = (property.Index, property.Length) switch - { - (null, null) => $"{baseIndex}, 1", - (null, { } length) when length == int.MaxValue => $"{baseIndex}, int.MaxValue", - (null, { } length) => $"{baseIndex}, {length}", - ({ } index, null) => $"{baseIndex + index}, 1", - ({ } index, { } length) when length == int.MaxValue => $"{baseIndex + index}, int.MaxValue", - ({ } index, { } length) => $"{baseIndex + index}, {length}", - }; - var exception = property.IsRequired - ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at {property.Index ?? 0}. Command line: {{commandLine}}\", \"{property.PropertyName}\")" - : (property.IsNullable, property.IsValueType) switch - { - (true, _) => "null", - (false, true) => "default", - (false, false) => "null!", - }; - if (property.IsRequired || property.IsInitOnly) - { - return $""" - {property.PropertyName} = commandLine.GetPositionalArgument({$"{indexLengthCode}"}){toMethod} ?? {exception}, -"""; - } - else - { - return $$""" - if (commandLine.GetPositionalArgument({{$"{indexLengthCode}"}}){{toMethod}} is { } p{{modelIndex}}) - { - result.{{property.PropertyName}} = p{{modelIndex}}; - } -"""; - } - } - - private string GenerateRawArgumentsPropertyAssignment(RawArgumentsPropertyGeneratingModel property) - { - var isInitProperty = property.IsRequired || property.IsInitOnly; - if (isInitProperty) - { - return $""" - {property.PropertyName} = (commandLine.CommandLineArguments as {property.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments], -"""; - } - else - { - return $$""" - result.{{property.PropertyName}} = (commandLine.CommandLineArguments as {{property.Type.ToDisplayString()}}) ?? [..commandLine.CommandLineArguments]; -"""; - } - } - - /// - /// 获取一个方法名,调用该方法可使“命令行属性值”转换为“目标类型”。 - /// - /// 目标类型。 - /// 方法名。 - private string? GetCommandLinePropertyValueToMethodName(ITypeSymbol targetType) - { - // 特殊处理接口,因为接口不支持隐式转换,所以要调用专门的转换方法。 - if (targetType.TypeKind is TypeKind.Interface) - { - return targetType.Name switch - { - "IEnumerable" or "IReadOnlyList" or "IList" or "ICollection" => "ToList", - "IReadOnlyDictionary" or "IDictionary" => "ToDictionary", - // 专门生成不存在的方法名和全名注释,编译不通过,同时还能辅助报告错误原因。 - _ => $"To{targetType.Name}/* {targetType.ToDisplayString()} */", - }; - } - - // 特殊处理枚举和可空枚举,因为枚举类型不可穷举,所以要调用专门的转换方法。 - if (targetType.ToDisplayString().EndsWith("?") && targetType.TypeKind is TypeKind.Struct) - { - // 拿到可空类型内部的类型,如 int? -> int。 - targetType = ((INamedTypeSymbol)targetType).TypeArguments[0]; - } - if (targetType.TypeKind is TypeKind.Enum) - { - return $"ToEnum<{targetType.ToNotNullGlobalDisplayString()}>"; - } - - // 其他类型使用隐式转换。 - return null; - } - - private string GenerateModuleInitializerCode(ImmutableArray models) - { - return $$""" -#nullable enable -namespace DotNetCampus.Cli; - -/// -/// 为本程序集中的所有命令行选项、子命令或处理函数编译时信息初始化。 -/// -internal static class CommandLineModuleInitializer -{ - [global::System.Runtime.CompilerServices.ModuleInitializerAttribute] - internal static void Initialize() - { -{{string.Join("\n\n", models.Select(GenerateCommandRunnerRegisterCode))}} - } -} - -"""; - } - - private string GenerateCommandRunnerRegisterCode(CommandObjectGeneratingModel model) - { - var commandCode = model.GetKebabCaseCommandNames() is { } vn ? $"\"{vn}\"" : "null"; - return $$""" - // {{model.CommandObjectType.Name}} { CommandName = {{commandCode}} } - global::DotNetCampus.Cli.CommandRunner.Register<{{model.CommandObjectType.ToGlobalDisplayString()}}>( - {{commandCode}}, - global::{{model.Namespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); -"""; - } - - private string GenerateAssemblyCommandHandlerCode(AssemblyCommandsGeneratingModel model, ImmutableArray models) - { - return $$""" -#nullable enable -namespace {{model.Namespace}}; - -#pragma warning disable CS0162 - -/// -/// 提供一种辅助自动搜集并执行本程序集中所有命令行处理器的方式。 -/// -partial class {{model.AssemblyCommandHandlerType.Name}} : global::DotNetCampus.Cli.Utils.Handlers.GeneratedAssemblyCommandHandlerCollection -{ - public {{model.AssemblyCommandHandlerType.Name}}() - { -{{string.Join("\n", models.GroupBy(x => x.GetKebabCaseCommandNames()).Select(GenerateAssemblyCommandHandlerMatchCode))}} - } -} - -"""; - } - - private string GenerateAssemblyCommandHandlerMatchCode(IGrouping group) - { - var models = group.ToList(); - if (models.Count is 1) - { - var model = models[0]; - if (model.IsHandler) - { - var assignment = group.Key is { } commandName ? $"Creators[\"{commandName}\"]" : "Default"; - return $""" - {assignment} = cl => (global::DotNetCampus.Cli.ICommandHandler)global::{model.Namespace}.{model.GetBuilderTypeName()}.CreateInstance(cl); -"""; - } - else - { - return $""" - // 类型 {model.CommandObjectType.Name} 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 -"""; - } - } - else - { - var commandName = group.Key is { } cn ? $"\"{cn}\"" : "null"; - return $""" - throw new global::DotNetCampus.Cli.Exceptions.CommandNameAmbiguityException($"Multiple command handlers match the same command name '{group.Key ?? "null"}': {string.Join(", ", models.Select(x => x.CommandObjectType.Name))}.", {commandName}); -"""; - } - } -} +// using System.Collections.Immutable; +// using DotNetCampus.CommandLine.Generators.ModelProviding; +// using DotNetCampus.CommandLine.Utils.CodeAnalysis; +// using Microsoft.CodeAnalysis; +// using Microsoft.CodeAnalysis.Diagnostics; +// +// namespace DotNetCampus.CommandLine.Generators; +// +// [Generator(LanguageNames.CSharp)] +// public class BuilderGenerator : IIncrementalGenerator +// { +// public void Initialize(IncrementalGeneratorInitializationContext context) +// { +// var analyzerConfigOptionsProvider = context.AnalyzerConfigOptionsProvider; +// var commandOptionsProvider = context.SelectCommandObjects(); +// var assemblyCommandsProvider = context.SelectAssemblyCommands(); +// +// context.RegisterSourceOutput( +// commandOptionsProvider, +// Execute); +// +// context.RegisterSourceOutput( +// assemblyCommandsProvider.Collect().Combine(commandOptionsProvider.Collect()).Combine(analyzerConfigOptionsProvider), +// Execute); +// } +// +// private void Execute(SourceProductionContext context, CommandObjectGeneratingModel model) +// { +// var code = GenerateCommandObjectCreatorCode(model); +// context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.cs", code); +// } +// +// private void Execute(SourceProductionContext context, +// ((ImmutableArray Left, ImmutableArray Right) Left, AnalyzerConfigOptionsProvider Right) +// args) +// { +// var ((assemblyCommandsGeneratingModels, commandOptionsGeneratingModels), analyzerConfigOptions) = args; +// commandOptionsGeneratingModels = [..commandOptionsGeneratingModels.OrderBy(x => x.GetBuilderTypeName())]; +// +// if (analyzerConfigOptions.GlobalOptions.TryGetValue("DotNetCampusCommandLineUseInterceptor", out var useInterceptor) +// && !useInterceptor) +// { +// var moduleInitializerCode = GenerateModuleInitializerCode(commandOptionsGeneratingModels); +// context.AddSource("CommandLine.Metadata/_ModuleInitializer.g.cs", moduleInitializerCode); +// } +// +// foreach (var assemblyCommandsGeneratingModel in assemblyCommandsGeneratingModels) +// { +// var code = GenerateAssemblyCommandHandlerCode(assemblyCommandsGeneratingModel, commandOptionsGeneratingModels); +// context.AddSource( +// $"CommandLine.Metadata/{assemblyCommandsGeneratingModel.Namespace}.{assemblyCommandsGeneratingModel.AssemblyCommandHandlerType.Name}.g.cs", +// code); +// } +// } +// +// private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) +// { +// // | required | nullable | cli | 行为 | +// // | -------- | -------- | --- | ---------- | +// // | 0 | 0 | 0 | 分析器警告 | +// // | 1 | 0 | 0 | 抛异常 | +// // | 0 | 1 | 0 | 默认值 | +// // | 1 | 1 | 0 | 抛异常 | +// // | 0 | 0 | 1 | 赋值 | +// // | 1 | 0 | 1 | 赋值 | +// // | 0 | 1 | 1 | 赋值 | +// // | 1 | 1 | 1 | 赋值 | +// +// var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); +// var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); +// var initValueProperties = model.ValueProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); +// var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); +// var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); +// var setValueProperties = model.ValueProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); +// return $$""" +// #nullable enable +// namespace {{model.Namespace}}; +// +// /// +// /// 辅助 生成命令行选项、子命令或处理函数的创建。 +// /// +// {{(model.IsPublic ? "public" : "internal")}} sealed class {{model.GetBuilderTypeName()}} +// { +// public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) +// { +// var caseSensitive = commandLine.DefaultCaseSensitive; +// var result = new {{model.CommandObjectType.ToGlobalDisplayString()}} +// { +// // 1. [RawArguments] +// {{(initRawArgumentsProperties.Length is 0 ? " // MainArgs = commandLine.CommandLineArguments," : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} +// +// // 2. [Option] +// {{(initOptionProperties.Length is 0 ? " // There is no option to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateOptionPropertyAssignment)))}} +// +// // 3. [Value] +// {{(initValueProperties.Length is 0 ? " // There is no positional argument to be initialized." : string.Join("\n", initValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} +// }; +// +// // 1. [RawArguments] +// {{(setRawArgumentsProperties.Length is 0 ? " // result.MainArgs = commandLine.CommandLineArguments;" : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} +// +// // 2. [Option] +// {{(setOptionProperties.Length is 0 ? " // There is no option to be assigned." : string.Join("\n", setOptionProperties.Select(GenerateOptionPropertyAssignment)))}} +// +// // 3. [Value] +// {{(setValueProperties.Length is 0 ? " // There is no positional argument to be assigned." : string.Join("\n", setValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} +// +// return result; +// } +// } +// +// """; +// } +// +// private string GenerateOptionPropertyAssignment(OptionPropertyGeneratingModel property, int modelIndex) +// { +// var isInitProperty = property.IsRequired || property.IsInitOnly; +// var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; +// var caseSensitive = property.CaseSensitive switch +// { +// true => ", true", +// false => ", false", +// null => "", +// }; +// var exception = property.IsRequired +// ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{property.GetDisplayCommandOption()}'. Command line: {{commandLine}}\", \"{property.PropertyName}\")" +// : (property.IsNullable, property.IsValueType) switch +// { +// (true, _) => "null", +// (false, true) => "default", +// (false, false) => "null!", +// }; +// +// var getters = property.GenerateAllNames( +// shortOption => $"""commandLine.GetShortOption("{shortOption}"{caseSensitive})""", +// longOption => $"""commandLine.GetOption("{longOption}"{caseSensitive})""", +// (caseSensitiveLongOption, ignoreCaseLongName) => +// $"""commandLine.GetOption(caseSensitive ? "{caseSensitiveLongOption}" : "{ignoreCaseLongName}"{caseSensitive})""", +// aliasOption => $"""commandLine.GetOption("{aliasOption}")""" +// ); +// +// return (isInitProperty, getters) switch +// { +// // [Option("OptionName")] +// // public required string PropertyName { get; init; } +// (true, { Count: 1 }) => $""" +// {property.PropertyName} = {getters[0]}{toMethod} ?? {exception}, +// """, +// // [Option('o', "OptionName")] +// // public required string PropertyName { get; init; } +// (true, _) => $""" +// {property.PropertyName} = ({string.Join("\n ?? ", getters)}){toMethod} +// ?? {exception}, +// """, +// // [Option("OptionName")] +// // public string PropertyName { get; set; } +// (false, { Count: 1 }) => $$""" +// if ({{getters[0]}}{{toMethod}} is { } o{{modelIndex}}) +// { +// result.{{property.PropertyName}} = o{{modelIndex}}; +// } +// """, +// // [Option('o', "OptionName")] +// // public string PropertyName { get; set; } +// (false, _) => $$""" +// if (({{string.Join("\n ?? ", getters)}}){{toMethod}} is { } o{{modelIndex}}) +// { +// result.{{property.PropertyName}} = o{{modelIndex}}; +// } +// """, +// }; +// } +// +// private string GenerateValuePropertyAssignment(CommandObjectGeneratingModel model, ValuePropertyGeneratingModel property, int modelIndex) +// { +// var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; +// var baseIndex = model.GetCommandLevel(); +// var indexLengthCode = (property.Index, property.Length) switch +// { +// (null, null) => $"{baseIndex}, 1", +// (null, { } length) when length == int.MaxValue => $"{baseIndex}, int.MaxValue", +// (null, { } length) => $"{baseIndex}, {length}", +// ({ } index, null) => $"{baseIndex + index}, 1", +// ({ } index, { } length) when length == int.MaxValue => $"{baseIndex + index}, int.MaxValue", +// ({ } index, { } length) => $"{baseIndex + index}, {length}", +// }; +// var exception = property.IsRequired +// ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at {property.Index ?? 0}. Command line: {{commandLine}}\", \"{property.PropertyName}\")" +// : (property.IsNullable, property.IsValueType) switch +// { +// (true, _) => "null", +// (false, true) => "default", +// (false, false) => "null!", +// }; +// if (property.IsRequired || property.IsInitOnly) +// { +// return $""" +// {property.PropertyName} = commandLine.GetPositionalArgument({$"{indexLengthCode}"}){toMethod} ?? {exception}, +// """; +// } +// else +// { +// return $$""" +// if (commandLine.GetPositionalArgument({{$"{indexLengthCode}"}}){{toMethod}} is { } p{{modelIndex}}) +// { +// result.{{property.PropertyName}} = p{{modelIndex}}; +// } +// """; +// } +// } +// +// private string GenerateRawArgumentsPropertyAssignment(RawArgumentsPropertyGeneratingModel property) +// { +// var isInitProperty = property.IsRequired || property.IsInitOnly; +// if (isInitProperty) +// { +// return $""" +// {property.PropertyName} = (commandLine.CommandLineArguments as {property.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments], +// """; +// } +// else +// { +// return $$""" +// result.{{property.PropertyName}} = (commandLine.CommandLineArguments as {{property.Type.ToDisplayString()}}) ?? [..commandLine.CommandLineArguments]; +// """; +// } +// } +// +// /// +// /// 获取一个方法名,调用该方法可使“命令行属性值”转换为“目标类型”。 +// /// +// /// 目标类型。 +// /// 方法名。 +// private string? GetCommandLinePropertyValueToMethodName(ITypeSymbol targetType) +// { +// // 特殊处理接口,因为接口不支持隐式转换,所以要调用专门的转换方法。 +// if (targetType.TypeKind is TypeKind.Interface) +// { +// return targetType.Name switch +// { +// "IEnumerable" or "IReadOnlyList" or "IList" or "ICollection" => "ToList", +// "IReadOnlyDictionary" or "IDictionary" => "ToDictionary", +// // 专门生成不存在的方法名和全名注释,编译不通过,同时还能辅助报告错误原因。 +// _ => $"To{targetType.Name}/* {targetType.ToDisplayString()} */", +// }; +// } +// +// // 特殊处理枚举和可空枚举,因为枚举类型不可穷举,所以要调用专门的转换方法。 +// if (targetType.ToDisplayString().EndsWith("?") && targetType.TypeKind is TypeKind.Struct) +// { +// // 拿到可空类型内部的类型,如 int? -> int。 +// targetType = ((INamedTypeSymbol)targetType).TypeArguments[0]; +// } +// if (targetType.TypeKind is TypeKind.Enum) +// { +// return $"ToEnum<{targetType.ToNotNullGlobalDisplayString()}>"; +// } +// +// // 其他类型使用隐式转换。 +// return null; +// } +// +// private string GenerateModuleInitializerCode(ImmutableArray models) +// { +// return $$""" +// #nullable enable +// namespace DotNetCampus.Cli; +// +// /// +// /// 为本程序集中的所有命令行选项、子命令或处理函数编译时信息初始化。 +// /// +// internal static class CommandLineModuleInitializer +// { +// [global::System.Runtime.CompilerServices.ModuleInitializerAttribute] +// internal static void Initialize() +// { +// {{string.Join("\n\n", models.Select(GenerateCommandRunnerRegisterCode))}} +// } +// } +// +// """; +// } +// +// private string GenerateCommandRunnerRegisterCode(CommandObjectGeneratingModel model) +// { +// var commandCode = model.GetKebabCaseCommandNames() is { } vn ? $"\"{vn}\"" : "null"; +// return $$""" +// // {{model.CommandObjectType.Name}} { CommandName = {{commandCode}} } +// global::DotNetCampus.Cli.CommandRunner.Register<{{model.CommandObjectType.ToGlobalDisplayString()}}>( +// {{commandCode}}, +// global::{{model.Namespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); +// """; +// } +// +// private string GenerateAssemblyCommandHandlerCode(AssemblyCommandsGeneratingModel model, ImmutableArray models) +// { +// return $$""" +// #nullable enable +// namespace {{model.Namespace}}; +// +// #pragma warning disable CS0162 +// +// /// +// /// 提供一种辅助自动搜集并执行本程序集中所有命令行处理器的方式。 +// /// +// partial class {{model.AssemblyCommandHandlerType.Name}} : global::DotNetCampus.Cli.Utils.Handlers.GeneratedAssemblyCommandHandlerCollection +// { +// public {{model.AssemblyCommandHandlerType.Name}}() +// { +// {{string.Join("\n", models.GroupBy(x => x.GetKebabCaseCommandNames()).Select(GenerateAssemblyCommandHandlerMatchCode))}} +// } +// } +// +// """; +// } +// +// private string GenerateAssemblyCommandHandlerMatchCode(IGrouping group) +// { +// var models = group.ToList(); +// if (models.Count is 1) +// { +// var model = models[0]; +// if (model.IsHandler) +// { +// var assignment = group.Key is { } commandName ? $"Creators[\"{commandName}\"]" : "Default"; +// return $""" +// {assignment} = cl => (global::DotNetCampus.Cli.ICommandHandler)global::{model.Namespace}.{model.GetBuilderTypeName()}.CreateInstance(cl); +// """; +// } +// else +// { +// return $""" +// // 类型 {model.CommandObjectType.Name} 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 +// """; +// } +// } +// else +// { +// var commandName = group.Key is { } cn ? $"\"{cn}\"" : "null"; +// return $""" +// throw new global::DotNetCampus.Cli.Exceptions.CommandNameAmbiguityException($"Multiple command handlers match the same command name '{group.Key ?? "null"}': {string.Join(", ", models.Select(x => x.CommandObjectType.Name))}.", {commandName}); +// """; +// } +// } +// } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs new file mode 100644 index 00000000..291c2bd4 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -0,0 +1,212 @@ +using DotNetCampus.CommandLine.Generators.Builders; +using DotNetCampus.CommandLine.Generators.ModelProviding; +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators; + +/// +/// 为命令行参数的模型对象生成创建器代码。 +/// +[Generator(LanguageNames.CSharp)] +public class ModelBuilderGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var commandOptionsProvider = context.SelectCommandObjects(); + context.RegisterSourceOutput(commandOptionsProvider, Execute); + } + + private void Execute(SourceProductionContext context, CommandObjectGeneratingModel model) + { + var code = GenerateCommandObjectCreatorCode(model); + context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.cs", code); + } + + private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) + { + // 对于不同的属性类型,生成不同的代码。 + // required: 属性要求必须赋值 + // nullable: 属性允许为 null + // cli: 实际命令行参数是否传入 + + // | required | nullable | cli | 行为 | + // | -------- | -------- | --- | ---------- | + // | 0 | 0 | 0 | 分析器警告 | + // | 0 | 1 | 0 | 默认值 | + // | 1 | _ | 0 | 抛异常 | + // | _ | _ | 1 | 赋值 | + + var modifier = model.IsPublic ? "public" : "internal"; + var builder = new SourceTextBuilder(model.Namespace) + .Using("System") + .Using("DotNetCampus.Cli.Compiler") + .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t + .WithDocumentationComment($""" +/// +/// 辅助 生成命令行选项、子命令或处理函数的创建。 +/// +""") + .AddRawMembers(model.OptionProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) + .AddRawText(GenerateBuildCode(model)) + .AddRawText(GenerateMatchLongOptionCode(model)) + .AddRawText(GenerateMatchShortOptionCode(model)) + .AddRawText(GenerateMatchPositionalArgumentsCode(model)) + .AddRawText(GenerateAssignPropertyValueCode(model)) + .AddRawText(GenerateBuildCoreCode(model)) + ); + return builder.ToString(); + } + + private string GenerateOptionPropertyCode(string @namespace, OptionPropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch + { + CommandPropertyType.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.Enum => $"private {@namespace}.{model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", + _ => $"// 不支持解析类型为 {model.Type.ToDisplayString()} 的属性 {model.PropertyName}。", + }; + + private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $$""" + public {{model.CommandObjectType.ToUsingString()}} Build() + { + var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(commandLine, "{{model.CommandObjectType.Name}}", {{model.GetCommandLevel()}}) + { + MatchLongOption = MatchLongOption, + MatchShortOption = MatchShortOption, + MatchPositionalArguments = MatchPositionalArguments, + AssignPropertyValue = AssignPropertyValue, + }; + parser.Parse(); + return BuildCore(); + } + """; + + private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) + { + var optionProperties = model.OptionProperties; + return $$""" + private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy) + { + var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; + + // 先原样匹配一遍。 + if (namingPolicy.SupportsOrdinal()) + { + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetOrdinalLongNames())))}} + } + + // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 + if (namingPolicy.SupportsPascalCase()) + { + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames().Except(x.GetOrdinalLongNames()).ToList())))}} + } + + return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; + } + """; + + static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) + { + if (names.Count == 0) + { + return $""" + // 属性 {model.PropertyName} 在此命名法下的所有名称均已在前面匹配过,无需重复匹配。 + """; + } + var comparison = model.CaseSensitive switch + { + true => "global::System.StringComparison.Ordinal", + false => "global::System.StringComparison.OrdinalIgnoreCase", + null => "defaultComparison", + }; + return string.Join("\n", names.Select(name => $$""" + if (longOption.Equals("{{name}}".AsSpan(), {{comparison}})) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); + } + """)); + } + } + + private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) + { + var optionProperties = model.OptionProperties; + return $$""" + private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) + { + var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; + + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} + + return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; + } + """; + + static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) + { + if (names.Count == 0) + { + return $""" + // 属性 {model.PropertyName} 没有短名称,无需匹配。 + """; + } + var comparison = model.CaseSensitive switch + { + true => "global::System.StringComparison.Ordinal", + false => "global::System.StringComparison.OrdinalIgnoreCase", + null => "defaultComparison", + }; + return string.Join("\n", names.Select(name => $$""" + if (shortOption.Equals("{{name}}".AsSpan(), {{comparison}})) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); + } + """)); + } + } + + private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel model) + { + return $$""" + private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) + { + } + """; + } + + private string GenerateAssignPropertyValueCode(CommandObjectGeneratingModel model) + { + return $$""" + private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) + { + } + """; + } + + private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) + { + return $$""" + private {{model.CommandObjectType.ToUsingString()}} BuildCore() + { + } + """; + } +} + +file static class Extensions +{ + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol type) + { + return $"__GeneratedEnumArgument__{type.ToDisplayString().Replace('.', '_')}__"; + } + + public static string ToOptionValueTypeName(this CommandPropertyType type) => type switch + { + CommandPropertyType.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", + CommandPropertyType.List => "global::DotNetCampus.Cli.OptionValueType.List", + CommandPropertyType.Dictionary => "global::DotNetCampus.Cli.OptionValueType.Dictionary", + _ => "global::DotNetCampus.Cli.OptionValueType.Normal", + }; +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 907d6902..90ca70d0 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -74,6 +74,15 @@ public static IncrementalValuesProvider SelectComm var commandNames = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString(); var isPublic = typeSymbol.DeclaredAccessibility == Accessibility.Public; + for (var i = 0; i < optionProperties.Length; i++) + { + optionProperties[i].PropertyIndex = i; + } + for (var i = 0; i < valueProperties.Length; i++) + { + valueProperties[i].PropertyIndex = i + optionProperties.Length; + } + return new CommandObjectGeneratingModel { Namespace = @namespace, @@ -176,94 +185,73 @@ internal record OptionPropertyGeneratingModel public required bool IsValueType { get; init; } - public required char? ShortName { get; init; } + public required IReadOnlyList ShortNames { get; init; } - public required string? LongName { get; init; } + public required IReadOnlyList LongNames { get; init; } public required bool? CaseSensitive { get; init; } - public required bool ExactSpelling { get; init; } - - public required ImmutableArray Aliases { get; init; } + public int PropertyIndex { get; set; } = -1; - public ImmutableArray GetNormalizedLongNames() + /// + /// 返回开发者定义的长选项名称列表,按定义顺序返回。
+ /// 如果没有定义,则返回 kebab-case 风格的属性名作为默认名称; + /// 如果有定义,无论定义了什么,都视其为 kebab-case 风格的名称。 + ///
+ public IReadOnlyList GetOrdinalLongNames() { - if (ExactSpelling) + List list = []; + if (LongNames.Count is 0) { - return [LongName ?? PropertyName]; + list.Add(NamingHelper.MakeKebabCase(PropertyName)); } - - return (CaseSensitive, LongName) switch + else { - // 如果没有指定长名称,那么长名称就是根据属性名推测的,这时一定自动将其转换为 kebab-case 小写风格。 - (_, null) => [NamingHelper.MakeKebabCase(LongName ?? PropertyName, true, true)], - - // 如果指定了大小写敏感,那么在转换为 kebab-case 时,不转换大小写。 - (true, _) => [NamingHelper.MakeKebabCase(LongName, true, false)], - - // 如果指定了大小写不敏感,那么在转换为 kebab-case 时,统一转换为小写。 - (false, _) => [NamingHelper.MakeKebabCase(LongName, true, true)], - - // 如果没有在属性处指定大小写敏感,那么给出两个转换的候选,延迟到运行时再决定。 - (null, _) => - [ - ..new List + foreach (var longName in LongNames) + { + if (!string.IsNullOrEmpty(longName) && !list.Contains(longName, StringComparer.Ordinal)) { - NamingHelper.MakeKebabCase(LongName, true, false), - NamingHelper.MakeKebabCase(LongName, true, true), - }.Distinct(StringComparer.Ordinal), - ], - }; + list.Add(longName); + } + } + } + return list; } - public string GetDisplayCommandOption() + public IReadOnlyList GetPascalCaseLongNames() { - var caseSensitive = CaseSensitive is true; - - if (LongName is { } longName) + List list = []; + if (LongNames.Count is 0) { - return $"--{NamingHelper.MakeKebabCase(longName, !caseSensitive, !caseSensitive)}"; + list.Add(PropertyName); } - - if (ShortName is { } shortName) + else { - return $"-{shortName}"; + foreach (var longName in LongNames) + { + if (!string.IsNullOrEmpty(longName)) + { + var pascalCase = NamingHelper.MakePascalCase(longName); + if (!list.Contains(pascalCase, StringComparer.Ordinal)) + { + list.Add(pascalCase); + } + } + } } - - return $"--{NamingHelper.MakeKebabCase(PropertyName, !caseSensitive, !caseSensitive)}"; + return list; } - public IReadOnlyList GenerateAllNames( - Func shortNameCreator, - Func longNameCreator, - Func caseLongNameCreator, - Func aliasCreator) + public IReadOnlyList GetShortNames() { - var list = new List(); - - if (ShortName is { } shortName) - { - list.Add(shortNameCreator(shortName.ToString(CultureInfo.InvariantCulture))); - } - - var longNames = GetNormalizedLongNames(); - if (longNames.Length is 1) - { - list.Add(longNameCreator(longNames[0])); - } - else if (longNames.Length is 2) + List list = []; + foreach (var shortName in ShortNames) { - list.Add(caseLongNameCreator(longNames[0], longNames[1])); - } - - if (Aliases is { Length: > 0 } aliases) - { - foreach (var alias in aliases) + if (!string.IsNullOrEmpty(shortName) && !list.Contains(shortName, StringComparer.Ordinal)) { - list.Add(aliasCreator(alias)); + list.Add(shortName); } } - return list; } @@ -275,18 +263,90 @@ public IReadOnlyList GenerateAllNames( return null; } - var longName = optionAttribute.ConstructorArguments.FirstOrDefault(x => x.Type?.SpecialType is SpecialType.System_String).Value?.ToString(); - var shortName = optionAttribute.ConstructorArguments.FirstOrDefault(x => x.Type?.SpecialType is SpecialType.System_Char).Value?.ToString(); - var caseSensitive = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.CaseSensitive)).Value.Value?.ToString(); - var exactSpelling = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.ExactSpelling)).Value.Value is true; - var aliases = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.Aliases)).Value switch + if (optionAttribute.ConstructorArguments.Length is 0) { - { Kind: TypedConstantKind.Array } typedConstant => typedConstant.Values.Select(a => a.Value?.ToString()) - .Where(a => !string.IsNullOrEmpty(a)) - .OfType() - .ToImmutableArray(), - _ => [], - }; + // 必须至少有一个构造函数参数。 + return null; + } + + List shortNames = []; + List longNames = []; + + if (optionAttribute.ConstructorArguments.Length is 1) + { + // 只有一个构造函数参数时,要么是短名称(一定是字符),要么是长名称(一定是字符串)。 + var arg = optionAttribute.ConstructorArguments[0]; + if (arg.Type?.SpecialType is SpecialType.System_Char) + { + var shortName = arg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (arg.Type?.SpecialType is SpecialType.System_String) + { + var longName = arg.Value?.ToString(); + if (!string.IsNullOrEmpty(longName)) + { + longNames.Add(longName!); + } + } + } + else if (optionAttribute.ConstructorArguments.Length is 2) + { + // 有两个构造函数参数时,第一个参数是短名称(字符、字符串、字符串数组),第二个参数是长名称(字符串、字符串数组)。 + var shortArg = optionAttribute.ConstructorArguments[0]; + if (shortArg.Type?.SpecialType is SpecialType.System_Char) + { + var shortName = shortArg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (shortArg.Type?.SpecialType is SpecialType.System_String) + { + var shortName = shortArg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (shortArg.Kind is TypedConstantKind.Array) + { + foreach (var value in shortArg.Values) + { + var shortName = value.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName) && !shortNames.Contains(shortName, StringComparer.Ordinal)) + { + shortNames.Add(shortName!); + } + } + } + var longArg = optionAttribute.ConstructorArguments[1]; + if (longArg.Type?.SpecialType is SpecialType.System_String) + { + var longName = longArg.Value?.ToString(); + if (!string.IsNullOrEmpty(longName)) + { + longNames.Add(longName!); + } + } + else if (longArg.Kind is TypedConstantKind.Array) + { + foreach (var value in longArg.Values) + { + var longName = value.Value?.ToString(); + if (!string.IsNullOrEmpty(longName) && !longNames.Contains(longName, StringComparer.Ordinal)) + { + longNames.Add(longName!); + } + } + } + } + + var caseSensitive = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.CaseSensitive)).Value.Value?.ToString(); return new OptionPropertyGeneratingModel { @@ -296,11 +356,9 @@ public IReadOnlyList GenerateAllNames( IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, IsValueType = propertySymbol.Type.IsValueType, - ShortName = shortName?.Length == 1 ? shortName[0] : null, - LongName = longName, + ShortNames = shortNames, + LongNames = longNames, CaseSensitive = caseSensitive is not null && bool.TryParse(caseSensitive, out var result) ? result : null, - ExactSpelling = exactSpelling, - Aliases = aliases, }; } } @@ -323,6 +381,8 @@ internal record ValuePropertyGeneratingModel public required int? Length { get; init; } + public int PropertyIndex { get; set; } = -1; + public static ValuePropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) { var valueAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); @@ -391,6 +451,99 @@ internal record AssemblyCommandsGeneratingModel public required INamedTypeSymbol AssemblyCommandHandlerType { get; init; } } +internal static class CommandModelExtensions +{ + public static CommandPropertyType AsCommandPropertyType(this ITypeSymbol typeSymbol) + { + if (typeSymbol.SpecialType is SpecialType.System_Boolean) + { + return CommandPropertyType.Boolean; + } + + if (typeSymbol.SpecialType is SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_Int32 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_Decimal) + { + return CommandPropertyType.Number; + } + + if (typeSymbol.TypeKind is TypeKind.Enum) + { + return CommandPropertyType.Enum; + } + + if (typeSymbol.SpecialType is SpecialType.System_String) + { + return CommandPropertyType.String; + } + + if (typeSymbol is INamedTypeSymbol + { + IsGenericType: true, TypeArguments: + [ + { SpecialType: SpecialType.System_String }, + ], + } namedTypeSymbol) + { + var genericTypeName = namedTypeSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + if (genericTypeName + is "System.Collections.Generic.IList<>" + or "System.Collections.Generic.IReadOnlyList<>" + or "System.Collections.Generic.ICollection<>" + or "System.Collections.Generic.IReadOnlyCollection<>" + or "System.Collections.Generic.IEnumerable<>" + or "System.Collections.Immutable.ImmutableArray<>" + or "System.Collections.Immutable.ImmutableHashSet<>" + or "System.Collections.ObjectModel.Collection<>" + or "System.Collections.Generic.List<>") + { + return CommandPropertyType.List; + } + } + + if (typeSymbol is INamedTypeSymbol + { + IsGenericType: true, TypeArguments: + [ + { SpecialType: SpecialType.System_String }, + { SpecialType: SpecialType.System_String }, + ], + } namedTypeSymbol2) + { + var genericTypeName = namedTypeSymbol2.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + if (genericTypeName + is "System.Collections.Generic.IDictionary<,>" + or "System.Collections.Generic.IReadOnlyDictionary<,>" + or "System.Collections.Immutable.ImmutableDictionary<,>" + or "System.Collections.Generic.Dictionary<,>" + or "System.Collections.Generic.KeyValuePair<,>") + { + return CommandPropertyType.Dictionary; + } + } + + return CommandPropertyType.String; + } +} + +internal enum CommandPropertyType +{ + Boolean, + Number, + Enum, + String, + List, + Dictionary, +} + file static class Extensions { public static IEnumerable EnumerateBaseTypesRecursively(this ITypeSymbol type) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index dc55b4ce..d723d68f 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -72,7 +72,7 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, - NamingPolicy = CommandNamingPolicy.CamelCase, + NamingPolicy = CommandNamingPolicy.PascalCase, OptionPrefix = CommandOptionPrefix.SingleDash, OptionValueSeparators = CommandSeparatorChars.Create(' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), @@ -90,7 +90,7 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = true, SupportsShortOptionValueWithoutSeparator = false, - NamingPolicy = CommandNamingPolicy.CamelCase, + NamingPolicy = CommandNamingPolicy.PascalCase, OptionPrefix = CommandOptionPrefix.Slash, OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), @@ -130,14 +130,14 @@ public CommandNamingPolicy NamingPolicy { (true, true) => CommandNamingPolicy.Both, (true, false) => CommandNamingPolicy.KebabCase, - (false, true) => CommandNamingPolicy.CamelCase, + (false, true) => CommandNamingPolicy.PascalCase, (false, false) => CommandNamingPolicy.Ordinal, }; init => _booleans[0, 1] = value switch { CommandNamingPolicy.Both => (true, true), CommandNamingPolicy.KebabCase => (true, false), - CommandNamingPolicy.CamelCase => (false, true), + CommandNamingPolicy.PascalCase => (false, true), CommandNamingPolicy.Ordinal => (false, false), _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), }; @@ -265,7 +265,7 @@ public enum CommandNamingPolicy : byte /// /// PascalCase/camelCase 风格命名。 /// - CamelCase = 1, + PascalCase = 1, /// /// kebab-case 风格命名。 @@ -278,7 +278,7 @@ public enum CommandNamingPolicy : byte /// /// 以 kebab-case 命名风格为主,兼顾支持 PascalCase/camelCase。 /// - Both = CamelCase | KebabCase, + Both = PascalCase | KebabCase, } /// @@ -328,7 +328,7 @@ public enum OptionValueType : byte /// /// 集合值。会尝试解析多个参数,直到遇到下一个选项或位置参数分隔符为止。 /// - Collection, + List, /// /// 字典值。会尝试解析多个键值对,直到遇到下一个选项或位置参数分隔符为止。 diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs b/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs index 08c0cee2..bd634df2 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandLineParsingExtensions.cs @@ -29,11 +29,11 @@ public static bool SupportsOrdinal(this CommandNamingPolicy namingPolicy) /// /// 命名风格。 /// 如果支持 PascalCase/camelCase 命名法,则返回 ;否则返回 - public static bool SupportsCamelCase(this CommandNamingPolicy namingPolicy) + public static bool SupportsPascalCase(this CommandNamingPolicy namingPolicy) { return namingPolicy switch { - CommandNamingPolicy.CamelCase => true, + CommandNamingPolicy.PascalCase => true, CommandNamingPolicy.Both => true, _ => false, }; diff --git a/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs index a11e0bff..885b88d6 100644 --- a/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs @@ -43,92 +43,111 @@ public sealed class OptionAttribute : CommandLineAttribute /// public OptionAttribute() { + ShortNames = []; + LongNames = []; } /// - /// 标记一个属性为命令行选项,并具有指定的长名称。 + /// 标记一个属性为命令行选项,并具有指定的短名称。 /// /// 选项的短名称。必须是单个字符。 public OptionAttribute(char shortName) { - if (!char.IsLetter(shortName)) - { - throw new ArgumentException($"选项的短名称必须是字母字符,但实际为 '{shortName}'。", nameof(shortName)); - } - - ShortName = shortName; + ShortNames = [shortName.ToString()]; + LongNames = []; } /// /// 标记一个属性为命令行选项,并具有指定的长名称。 /// - /// - /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 - /// + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 public OptionAttribute(string longName) { - LongName = longName; + ShortNames = []; + LongNames = [longName]; } /// /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 /// /// 选项的短名称。必须是单个字符。 - /// - /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 - /// + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 public OptionAttribute(char shortName, string longName) { - if (!char.IsLetter(shortName)) - { - throw new ArgumentException($"选项的短名称必须是字母字符,但实际为 '{shortName}'。", nameof(shortName)); - } + ShortNames = [shortName.ToString()]; + LongNames = [longName]; + } - LongName = longName; - ShortName = shortName; + /// + /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 + /// + /// 选项的短名称。必须是单个字符。 + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 + public OptionAttribute(char shortName, string[] longNames) + { + ShortNames = [shortName.ToString()]; + LongNames = longNames; } /// - /// 获取或初始化选项的短名称。 + /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 /// - public char ShortName { get; } = '\0'; + /// 支持多字符的多个短名称,如用 -tl 来表示 --terminal-logger。 + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 + public OptionAttribute(string shortName, string longName) + { + ShortNames = [shortName]; + LongNames = [longName]; + } /// - /// 获取选项的长名称。 + /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 /// - public string? LongName { get; } + /// 支持多字符的多个短名称,如用 -tl 来表示 --terminal-logger。 + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 + public OptionAttribute(string shortName, string[] longNames) + { + ShortNames = [shortName]; + LongNames = longNames; + } /// - /// 获取或设置选项的别名。 + /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 /// - /// - /// 可以指定短名称(如 `v`)或长名称(如 `verbose`)。单个字符的别名会被视为短名称。
- /// 如果指定区分大小写,但期望允许部分单词使用多种大小写,则应该在别名中指定多个大小写形式。如将 `verbose` 的别名指定为 `verbose Verbose VERBOSE`。 - ///
- public string[] Aliases { get; init; } = []; + /// 支持多字符的多个短名称,如用 -tl 来表示 --terminal-logger。 + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 + public OptionAttribute(string[] shortNames, string longName) + { + ShortNames = shortNames; + LongNames = [longName]; + } /// - /// 获取或设置是否大小写敏感。 + /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 /// - /// - /// 默认情况下使用 解析时所指定的大小写敏感性(而 默认为大小写不敏感)。 - /// - public bool CaseSensitive { get; init; } + /// 支持多字符的多个短名称,如用 -tl 来表示 --terminal-logger。 + /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 + public OptionAttribute(string[] shortNames, string[] longNames) + { + ShortNames = shortNames; + LongNames = longNames; + } + + /// + /// 获取或初始化选项的短名称。 + /// + public string[] ShortNames { get; } + + /// + /// 获取选项的长名称。 + /// + public string[] LongNames { get; } /// - /// 命令行参数中传入的选项名称必须严格保持与此属性中指定的长名称一致。 + /// 获取或设置是否大小写敏感。 /// /// - /// 默认情况下,我们会为了支持多种不同的命令行风格而自动识别选项的长名称,例如: - /// - /// 属性名 SampleProperty 可匹配:--Sample-Property --sample-property -SampleProperty - /// 属性名 sample-property 可匹配:--Sample-Property --sample-property -SampleProperty - /// - /// 但设置了此属性为 后,命令行中传入的选项名称必须完全一致: - /// - /// 属性名 SampleProperty 可匹配:--SampleProperty --sampleproperty -SampleProperty - /// 属性名 sample-property 可匹配:--Sample-Property --sample-property -Sample-Property - /// + /// 默认情况下使用 解析时所指定的大小写敏感性(而 默认为大小写不敏感)。 /// - public bool ExactSpelling { get; init; } + public bool CaseSensitive { get; init; } } diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index c04be8df..97e2c35a 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -261,7 +261,7 @@ public void Append(ReadOnlySpan value) /// /// 专门解析来自命令行的字典类型,并辅助赋值给属性。 /// -public struct DictionaryArgument +public struct StringDictionaryArgument { /// /// 存储解析到的字符串字典。 diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 3568230f..54b8c260 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -131,7 +131,7 @@ public CommandLineParsingResult Parse() case Cat.OptionValue: { AssignOptionValue(currentOption, value); - if (currentOption.ValueType is not OptionValueType.Collection) + if (currentOption.ValueType is not OptionValueType.List) { // 如果不是集合,那么此选项已经结束。 // 清空上一个选项,避免误用。 @@ -226,7 +226,7 @@ public CommandLineParsingResult Parse() private void AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) { - if (match.ValueType is OptionValueType.Collection) + if (match.ValueType is OptionValueType.List) { Span separators = stackalloc char[4]; Style.CollectionValueSeparators.CopyTo(separators, out var length); @@ -407,7 +407,7 @@ private bool ParseOptionAndPositionalArgumentRegion() // 如果是布尔选项,则后面只能跟布尔值,否则只能是新的选项或位置参数。 OptionValueType.Boolean => ParseBooleanOptionValueOrNewOptionOrPositionalArgument(), // 如果是集合选项,则后面可以跟多个值,直到遇到新的选项或位置参数分隔符为止。 - OptionValueType.Collection or OptionValueType.Dictionary => ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), + OptionValueType.List or OptionValueType.Dictionary => ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), // 如果是普通选项,则后面只能是选项值。 _ => ParseOptionValue(_argument.AsSpan()), }), From 5edb306bf096ee898e1c7245c4c6a02427114e80 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 20:37:55 +0800 Subject: [PATCH 016/193] =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=92=8C=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 80 ++++++++++++------- .../ModelProviding/CommandModelProvider.cs | 13 ++- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 291c2bd4..20685fa8 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -48,9 +48,21 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod """) .AddRawMembers(model.OptionProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) .AddRawText(GenerateBuildCode(model)) - .AddRawText(GenerateMatchLongOptionCode(model)) - .AddRawText(GenerateMatchShortOptionCode(model)) - .AddRawText(GenerateMatchPositionalArgumentsCode(model)) + .AddMethodDeclaration( + "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", + m => m + .AddRawStatements(GenerateMatchLongOptionCode(model)) + .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;")) + .AddMethodDeclaration( + "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive)", + m => m + .AddRawStatements(GenerateMatchShortOptionCode(model)) + .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;")) + .AddMethodDeclaration( + "private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex)", + m => m + .AddRawStatements(GenerateMatchPositionalArgumentsCode(model)) + .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) .AddRawText(GenerateAssignPropertyValueCode(model)) .AddRawText(GenerateBuildCoreCode(model)) ); @@ -86,26 +98,23 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return $$""" - private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy) - { + return optionProperties.Length is 0 + ? "// 没有长名称选项,无需匹配。" + : $$""" var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; - + // 先原样匹配一遍。 if (namingPolicy.SupportsOrdinal()) { - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetOrdinalLongNames())))}} + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetOrdinalLongNames())))}} } // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 if (namingPolicy.SupportsPascalCase()) { - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames().Except(x.GetOrdinalLongNames()).ToList())))}} + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames().Except(x.GetOrdinalLongNames()).ToList())))}} } - - return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; - } - """; + """; static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) { @@ -113,7 +122,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead { return $""" // 属性 {model.PropertyName} 在此命名法下的所有名称均已在前面匹配过,无需重复匹配。 - """; + """; } var comparison = model.CaseSensitive switch { @@ -126,23 +135,20 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead { return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); } - """)); + """)); } } private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return $$""" - private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) - { + return optionProperties.Length is 0 + ? "// 没有短名称选项,无需匹配。" + : $$""" var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} - - return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; - } - """; + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} + """; static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) { @@ -150,7 +156,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead { return $""" // 属性 {model.PropertyName} 没有短名称,无需匹配。 - """; + """; } var comparison = model.CaseSensitive switch { @@ -163,16 +169,34 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead { return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); } - """)); + """)); } } private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel model) { - return $$""" - private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) - { + var positionalArgumentProperties = model.PositionalArgumentProperties; + return positionalArgumentProperties.Length is 0 + ? "// 没有位置参数,无需匹配。" + : $$""" + {{string.Join("\n", positionalArgumentProperties.Select(x => GenerateMatchPositionalArgumentCode(x, x.Index, x.Length)))}} + """; } + + private string GenerateMatchPositionalArgumentCode(ValuePropertyGeneratingModel valuePropertyGeneratingModel, int index, int length) + { + return length == 1 + ? $$""" + if (argumentIndex is {{index}}) + { + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{valuePropertyGeneratingModel.PropertyName}}", {{valuePropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + } + """ + : $$""" + if (argumentIndex is >= {{index}} and < {{index + length}}) + { + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{valuePropertyGeneratingModel.PropertyName}}", {{valuePropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + } """; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 90ca70d0..1506cbce 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Globalization; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; using DotNetCampus.CommandLine.Utils.CodeAnalysis; @@ -91,7 +90,7 @@ public static IncrementalValuesProvider SelectComm CommandNames = commandNames, IsHandler = isHandler, OptionProperties = optionProperties, - ValueProperties = valueProperties, + PositionalArgumentProperties = valueProperties, RawArgumentsProperties = rawArgumentsProperties, }; }) @@ -143,7 +142,7 @@ internal record CommandObjectGeneratingModel public required ImmutableArray OptionProperties { get; init; } - public required ImmutableArray ValueProperties { get; init; } + public required ImmutableArray PositionalArgumentProperties { get; init; } public required ImmutableArray RawArgumentsProperties { get; init; } @@ -377,9 +376,9 @@ internal record ValuePropertyGeneratingModel public required bool IsValueType { get; init; } - public required int? Index { get; init; } + public required int Index { get; init; } - public required int? Length { get; init; } + public required int Length { get; init; } public int PropertyIndex { get; set; } = -1; @@ -407,8 +406,8 @@ internal record ValuePropertyGeneratingModel IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, IsValueType = propertySymbol.Type.IsValueType, - Index = index is not null && int.TryParse(index, out var result) ? result : null, - Length = length is not null && int.TryParse(length, out var result2) ? result2 : null, + Index = index is not null && int.TryParse(index, out var result) ? result : 0, + Length = length is not null && int.TryParse(length, out var result2) ? result2 : 1, }; } } From 420b19f5c867aac67b9590f9fd66d2f8bb91911a Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 22:37:19 +0800 Subject: [PATCH 017/193] =?UTF-8?q?=E7=94=9F=E6=88=90=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E7=9A=84=E6=A8=A1=E5=9E=8B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 4 +- .../Generators/ModelBuilderGenerator.cs | 175 ++++++++++++++---- .../ModelProviding/CommandModelProvider.cs | 114 +++++------- .../DotNetCommandLineParserTests.cs | 2 +- .../FlexibleCommandLineParserTests.cs | 2 +- .../GnuCommandLineParserTests.cs | 2 +- .../NamingConventionTests.cs | 10 +- .../PowerShellCommandLineParserTests.cs | 2 +- 8 files changed, 200 insertions(+), 111 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index 19ae42e1..57ba24c2 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -17,7 +17,9 @@ public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer private readonly HashSet _oneGenericTypeNames = [ - "[]", "ImmutableArray", "List", "IList", "IReadOnlyList", "ImmutableHashSet", "Collection", "ICollection", "IReadOnlyCollection", "IEnumerable", + "[]", + "IList", "ICollection", "IEnumerable", "IReadOnlyList", "IReadOnlyCollection", "ISet", "IImmutableSet", "IImmutableList", + "ImmutableArray", "List", "ImmutableHashSet", "Collection", "HashSet", ]; private readonly HashSet _rawArgumentsGenericTypeNames = diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 20685fa8..66d2c2f3 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -24,29 +24,14 @@ private void Execute(SourceProductionContext context, CommandObjectGeneratingMod private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) { - // 对于不同的属性类型,生成不同的代码。 - // required: 属性要求必须赋值 - // nullable: 属性允许为 null - // cli: 实际命令行参数是否传入 - - // | required | nullable | cli | 行为 | - // | -------- | -------- | --- | ---------- | - // | 0 | 0 | 0 | 分析器警告 | - // | 0 | 1 | 0 | 默认值 | - // | 1 | _ | 0 | 抛异常 | - // | _ | _ | 1 | 赋值 | - var modifier = model.IsPublic ? "public" : "internal"; var builder = new SourceTextBuilder(model.Namespace) .Using("System") .Using("DotNetCampus.Cli.Compiler") .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t - .WithDocumentationComment($""" -/// -/// 辅助 生成命令行选项、子命令或处理函数的创建。 -/// -""") + .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") .AddRawMembers(model.OptionProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) + .AddRawMembers(model.PositionalArgumentProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", @@ -63,17 +48,25 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod m => m .AddRawStatements(GenerateMatchPositionalArgumentsCode(model)) .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) - .AddRawText(GenerateAssignPropertyValueCode(model)) - .AddRawText(GenerateBuildCoreCode(model)) + .AddMethodDeclaration( + "private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value)", + m => m + .BeginBracketScope("switch (propertyIndex)", l => l + .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode)) + .AddRawStatements(model.PositionalArgumentProperties.Select(GenerateAssignPropertyValueCode)))) + .AddMethodDeclaration( + $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", + m => m + .AddRawStatements(GenerateBuildCoreCode(model))) ); return builder.ToString(); } - private string GenerateOptionPropertyCode(string @namespace, OptionPropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch + private string GenerateOptionPropertyCode(string @namespace, PropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch { CommandPropertyType.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.Enum => $"private {@namespace}.{model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.Enum => $"private global::{@namespace}.{model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", CommandPropertyType.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", @@ -91,7 +84,7 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ AssignPropertyValue = AssignPropertyValue, }; parser.Parse(); - return BuildCore(); + return BuildCore(commandLine); } """; @@ -183,39 +176,159 @@ private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel """; } - private string GenerateMatchPositionalArgumentCode(ValuePropertyGeneratingModel valuePropertyGeneratingModel, int index, int length) + private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGeneratingModel positionalArgumentPropertyGeneratingModel, int index, + int length) { return length == 1 ? $$""" if (argumentIndex is {{index}}) { - return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{valuePropertyGeneratingModel.PropertyName}}", {{valuePropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{positionalArgumentPropertyGeneratingModel.PropertyName}}", {{positionalArgumentPropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); } """ : $$""" if (argumentIndex is >= {{index}} and < {{index + length}}) { - return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{valuePropertyGeneratingModel.PropertyName}}", {{valuePropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{positionalArgumentPropertyGeneratingModel.PropertyName}}", {{positionalArgumentPropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); } """; } - private string GenerateAssignPropertyValueCode(CommandObjectGeneratingModel model) + private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) + { + var assign = model.Type.AsCommandPropertyType() switch + { + CommandPropertyType.Boolean => $"{model.PropertyName}.Assign(value[0] == '1');", + CommandPropertyType.List => $"{model.PropertyName}.Append(value);", + CommandPropertyType.Dictionary => $"{model.PropertyName}.Append(key, value);", + _ => $"{model.PropertyName}.Assign(value);", + }; + var propertyIndex = model switch + { + OptionPropertyGeneratingModel optionPropertyGeneratingModel => optionPropertyGeneratingModel.PropertyIndex, + PositionalArgumentPropertyGeneratingModel positionalArgumentPropertyGeneratingModel => positionalArgumentPropertyGeneratingModel.PropertyIndex, + _ => -1, + }; + return $""" + case {propertyIndex}: + {assign} + break; + """; + } + + private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) { + // 对于不同的属性类型,生成不同的代码。 + // init: 属性要求必须立即赋值 + // nullable: 属性允许为 null + // cli: 实际命令行参数是否传入 + + // | init | nullable | cli | 行为 | + // | ---- | -------- | --- | ---------- | + // | 1 | 1 | 0 | 默认值 | + // | 1 | 0 | 0 | 抛异常 | + // | 0 | 1 | 0 | 保留初值 | + // | 0 | 0 | 0 | 保留初值 | + // | _ | _ | 1 | 赋值 | + + var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); + var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); + var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); + var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); + var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); + var setPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); return $$""" - private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) + var result = new {{model.CommandObjectType.ToUsingString()}} { - } + // 1. [RawArguments] + {{( + initRawArgumentsProperties.Count is 0 + ? " // There is no [RawArguments] property to be initialized." + : string.Join("\n", initRawArgumentsProperties.Select(GenerateInitRawArgumentProperty)) + )}} + + // 2. [Option] + {{( + initOptionProperties.Count is 0 + ? " // There is no [Option] property to be initialized." + : string.Join("\n", initOptionProperties.Select(GenerateInitProperty)) + )}} + + // 3. [Value] + {{( + initPositionalArgumentProperties.Count is 0 + ? " // There is no [Value] property to be initialized." + : string.Join("\n", initPositionalArgumentProperties.Select(GenerateInitProperty)) + )}} + }; + + // 1. [RawArguments] + {{( + setRawArgumentsProperties.Count is 0 + ? "// There is no [RawArguments] property to be assigned." + : string.Join("\n", setRawArgumentsProperties.Select(GenerateSetRawArgumentProperty)) + )}} + + // 2. [Option] + {{( + setOptionProperties.Count is 0 + ? "// There is no [RawArguments] property to be assigned." + : string.Join("\n", setOptionProperties.Select(GenerateSetProperty)) + )}} + + // 3. [Value] + {{( + setPositionalArgumentProperties.Count is 0 + ? "// There is no [RawArguments] property to be assigned." + : string.Join("\n", setPositionalArgumentProperties.Select(GenerateSetProperty)) + )}} + + return result; """; } - private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) + private string GenerateInitProperty(PropertyGeneratingModel model) + { + var toTarget = model.Type.ToCommandTargetMethodName(); + var fallback = model.IsNullable + ? " ?? null" + : model switch + { + OptionPropertyGeneratingModel option => + $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", + PositionalArgumentPropertyGeneratingModel positionalArgument => + $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", + _ => "", + }; + return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){fallback},"; + } + + private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex) { + var toTarget = model.Type.ToCommandTargetMethodName(); + var variablePrefix = model switch + { + RawArgumentsPropertyGeneratingModel => "a", + OptionPropertyGeneratingModel => "o", + PositionalArgumentPropertyGeneratingModel => "v", + _ => "", + }; return $$""" - private {{model.CommandObjectType.ToUsingString()}} BuildCore() + if ({{model.PropertyName}}.To{{toTarget}}() is { } {{variablePrefix}}{{modelIndex}}) + { + result.{{model.PropertyName}} = {{variablePrefix}}{{modelIndex}}; + } + """; + } + + private string GenerateInitRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) { + return $" {model.PropertyName} = commandLine.CommandLineArguments,"; } - """; + + private string GenerateSetRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) + { + return $"result.{model.PropertyName} = commandLine.CommandLineArguments;"; } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 1506cbce..c9ceac00 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -58,7 +58,7 @@ public static IncrementalValuesProvider SelectComm .GetAttributedProperties(OptionPropertyGeneratingModel.TryParse); // 5. 拥有 [Value] 特性的属性。 var valueProperties = typeSymbol - .GetAttributedProperties(ValuePropertyGeneratingModel.TryParse); + .GetAttributedProperties(PositionalArgumentPropertyGeneratingModel.TryParse); // 6. 拥有 [RawArguments] 特性的属性。 var rawArgumentsProperties = typeSymbol .GetAttributedProperties(RawArgumentsPropertyGeneratingModel.TryParse); @@ -142,7 +142,7 @@ internal record CommandObjectGeneratingModel public required ImmutableArray OptionProperties { get; init; } - public required ImmutableArray PositionalArgumentProperties { get; init; } + public required ImmutableArray PositionalArgumentProperties { get; init; } public required ImmutableArray RawArgumentsProperties { get; init; } @@ -170,7 +170,7 @@ public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) } } -internal record OptionPropertyGeneratingModel +internal abstract record PropertyGeneratingModel { public required string PropertyName { get; init; } @@ -181,7 +181,10 @@ internal record OptionPropertyGeneratingModel public required bool IsInitOnly { get; init; } public required bool IsNullable { get; init; } +} +internal record OptionPropertyGeneratingModel : PropertyGeneratingModel +{ public required bool IsValueType { get; init; } public required IReadOnlyList ShortNames { get; init; } @@ -362,18 +365,8 @@ public IReadOnlyList GetShortNames() } } -internal record ValuePropertyGeneratingModel +internal record PositionalArgumentPropertyGeneratingModel : PropertyGeneratingModel { - public required string PropertyName { get; init; } - - public required ITypeSymbol Type { get; init; } - - public required bool IsRequired { get; init; } - - public required bool IsInitOnly { get; init; } - - public required bool IsNullable { get; init; } - public required bool IsValueType { get; init; } public required int Index { get; init; } @@ -382,7 +375,7 @@ internal record ValuePropertyGeneratingModel public int PropertyIndex { get; set; } = -1; - public static ValuePropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) + public static PositionalArgumentPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) { var valueAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); if (valueAttribute is null) @@ -398,7 +391,7 @@ internal record ValuePropertyGeneratingModel // 其次从构造函数参数中拿。 ?? valueAttribute.ConstructorArguments.ElementAtOrDefault(1).Value?.ToString(); - return new ValuePropertyGeneratingModel + return new PositionalArgumentPropertyGeneratingModel { PropertyName = propertySymbol.Name, Type = propertySymbol.Type, @@ -412,18 +405,8 @@ internal record ValuePropertyGeneratingModel } } -internal record RawArgumentsPropertyGeneratingModel +internal record RawArgumentsPropertyGeneratingModel : PropertyGeneratingModel { - public required string PropertyName { get; init; } - - public required ITypeSymbol Type { get; init; } - - public required bool IsRequired { get; init; } - - public required bool IsInitOnly { get; init; } - - public required bool IsNullable { get; init; } - public static RawArgumentsPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) { var rawArgumentsAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); @@ -452,6 +435,25 @@ internal record AssemblyCommandsGeneratingModel internal static class CommandModelExtensions { + private static readonly SymbolDisplayFormat ToTargetTypeFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.None, + kindOptions: SymbolDisplayKindOptions.None + ); + + public static string ToCommandTargetMethodName(this ITypeSymbol type) + { + // 取出类型的 .NET 类名称,不含泛型。如 bool 返回 Boolean,Dictionary 返回 Dictionary。 + return type.ToDisplayString(ToTargetTypeFormat) switch + { + "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" + or "IImmutableSet" or "IImmutableList" => "List", + "IDictionary" or "IReadOnlyDictionary" => "Dictionary", + var name => name, + }; + } + public static CommandPropertyType AsCommandPropertyType(this ITypeSymbol typeSymbol) { if (typeSymbol.SpecialType is SpecialType.System_Boolean) @@ -474,6 +476,11 @@ SpecialType.System_Double or return CommandPropertyType.Number; } + if (typeSymbol.SpecialType is SpecialType.System_Array) + { + return CommandPropertyType.List; + } + if (typeSymbol.TypeKind is TypeKind.Enum) { return CommandPropertyType.Enum; @@ -484,52 +491,15 @@ SpecialType.System_Double or return CommandPropertyType.String; } - if (typeSymbol is INamedTypeSymbol - { - IsGenericType: true, TypeArguments: - [ - { SpecialType: SpecialType.System_String }, - ], - } namedTypeSymbol) + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch { - var genericTypeName = namedTypeSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - if (genericTypeName - is "System.Collections.Generic.IList<>" - or "System.Collections.Generic.IReadOnlyList<>" - or "System.Collections.Generic.ICollection<>" - or "System.Collections.Generic.IReadOnlyCollection<>" - or "System.Collections.Generic.IEnumerable<>" - or "System.Collections.Immutable.ImmutableArray<>" - or "System.Collections.Immutable.ImmutableHashSet<>" - or "System.Collections.ObjectModel.Collection<>" - or "System.Collections.Generic.List<>") - { - return CommandPropertyType.List; - } - } - - if (typeSymbol is INamedTypeSymbol - { - IsGenericType: true, TypeArguments: - [ - { SpecialType: SpecialType.System_String }, - { SpecialType: SpecialType.System_String }, - ], - } namedTypeSymbol2) - { - var genericTypeName = namedTypeSymbol2.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - if (genericTypeName - is "System.Collections.Generic.IDictionary<,>" - or "System.Collections.Generic.IReadOnlyDictionary<,>" - or "System.Collections.Immutable.ImmutableDictionary<,>" - or "System.Collections.Generic.Dictionary<,>" - or "System.Collections.Generic.KeyValuePair<,>") - { - return CommandPropertyType.Dictionary; - } - } - - return CommandPropertyType.String; + "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" + or "IImmutableSet" or "IImmutableList" => CommandPropertyType.List, + "ImmutableArray" or "List" or "ImmutableHashSet" or "Collection" or "HashSet" => CommandPropertyType.List, + "IDictionary" or "IReadOnlyDictionary" => CommandPropertyType.Dictionary, + "ImmutableDictionary" or "Dictionary" => CommandPropertyType.Dictionary, + _ => CommandPropertyType.String, + }; } } diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs index 6914ddaf..d6c65370 100644 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs @@ -1015,7 +1015,7 @@ internal record DotNet11_CaseInsensitiveOptions internal record DotNet12_AliasOptions { - [Option("option-with-alias", Aliases = ["alt", "alternate"])] + [Option([], ["option-with-alias", "alt", "alternate"])] public string OptionWithAlias { get; init; } = string.Empty; } diff --git a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs index e0c7a423..5ea5b255 100644 --- a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs @@ -896,7 +896,7 @@ internal record Flexible07_BooleanOptions internal record Flexible08_NegatedBooleanOptions { - [Option("feature", Aliases = ["no-feature"])] + [Option([], ["feature", "no-feature"])] public bool Feature { get; init; } = true; } diff --git a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs index b80957c5..1a671486 100644 --- a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs @@ -948,7 +948,7 @@ internal record GNU11_CaseInsensitiveOptions internal record GNU12_AliasOptions { - [Option("option-with-alias", Aliases = ["alt", "alternate"])] + [Option([], ["option-with-alias", "alt", "alternate"])] public string OptionWithAlias { get; init; } = string.Empty; } diff --git a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs b/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs index 653bf19f..3c50ee9b 100644 --- a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs @@ -17,7 +17,11 @@ namespace DotNetCampus.Cli.Tests; public class NamingConventionTests { private CommandLineParsingOptions Flexible { get; } = CommandLineParsingOptions.Flexible; - private CommandLineParsingOptions CaseSensitive { get; } = new() { CaseSensitive = true }; + + private CommandLineParsingOptions CaseSensitive { get; } = new CommandLineParsingOptions + { + Style = CommandLineParsingOptions.Flexible.Style with { CaseSensitive = false }, + }; #region 1. CommandAttribute 命名规则测试 @@ -613,13 +617,13 @@ internal class BuildWithCombinedOptionsCommand internal class BuildWithAliasesCommand { - [Option("output-path", Aliases = ["out", "directory"])] + [Option([], ["output-path", "out", "directory"])] public required string OutputPath { get; init; } } internal class ExactSpellingCommand { - [Option("SampleProperty", ExactSpelling = true)] + [Option("SampleProperty")] public required string SampleProperty { get; init; } } diff --git a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs index af8068bc..6c87309d 100644 --- a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs @@ -634,7 +634,7 @@ internal record PS17_QuotedValueOptions internal record PS18_AliasOptions { - [Option("ParameterWithAlias", Aliases = ["Alias", "Alt"])] + [Option([], ["ParameterWithAlias", "Alias", "Alt"])] public string ParameterWithAlias { get; init; } = string.Empty; } From e9dcb12a361276a1f9f7688b30e4d271516d7e7d Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 23:38:35 +0800 Subject: [PATCH 018/193] =?UTF-8?q?=E7=94=9F=E6=88=90=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E8=B5=8B=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 59 +++++++++++++++++-- .../ModelProviding/CommandModelProvider.cs | 20 +++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 66d2c2f3..99f23635 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -30,8 +30,8 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .Using("DotNetCampus.Cli.Compiler") .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") - .AddRawMembers(model.OptionProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) - .AddRawMembers(model.PositionalArgumentProperties.Select(x => GenerateOptionPropertyCode(model.Namespace, x))) + .AddRawMembers(model.OptionProperties.Select(GenerateOptionPropertyCode)) + .AddRawMembers(model.PositionalArgumentProperties.Select(GenerateOptionPropertyCode)) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", @@ -58,15 +58,16 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", m => m .AddRawStatements(GenerateBuildCoreCode(model))) + .AddRawMembers(model.EnumerateEnumPropertyTypes().Select(GenerateEnumDeclarationCode)) ); return builder.ToString(); } - private string GenerateOptionPropertyCode(string @namespace, PropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch + private string GenerateOptionPropertyCode(PropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch { CommandPropertyType.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.Enum => $"private global::{@namespace}.{model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", + CommandPropertyType.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", CommandPropertyType.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", CommandPropertyType.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", @@ -139,7 +140,7 @@ private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) ? "// 没有短名称选项,无需匹配。" : $$""" var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; - + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} """; @@ -253,7 +254,7 @@ initOptionProperties.Count is 0 ? " // There is no [Option] property to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateInitProperty)) )}} - + // 3. [Value] {{( initPositionalArgumentProperties.Count is 0 @@ -330,6 +331,52 @@ private string GenerateSetRawArgumentProperty(RawArgumentsPropertyGeneratingMode { return $"result.{model.PropertyName} = commandLine.CommandLineArguments;"; } + + private string GenerateEnumDeclarationCode(ITypeSymbol enumType) + { + var enumNames = enumType.GetMembers().OfType().Select(x => x.Name); + return $$""" +/// +/// Provides parsing and assignment for the enum type . +/// +private struct {{enumType.GetGeneratedEnumArgumentTypeName()}} +{ + /// + /// Indicates whether to ignore exceptions when parsing fails. + /// + public bool IgnoreExceptions { get; init; } + + /// + /// Stores the parsed enum value. + /// + private {{enumType.ToUsingString()}}? _value; + + /// + /// Assigns a value when a command line input is parsed. + /// + /// The parsed string value. + public void Assign(ReadOnlySpan value) + { + Span lowerValue = stackalloc char[value.Length]; + for (var i = 0; i < value.Length; i++) + { + lowerValue[i] = char.ToLowerInvariant(value[i]); + } + _value = lowerValue switch + { + {{string.Join("\n ", enumNames.Select(x => $" \"{x.ToLowerInvariant()}\" => {enumType.ToUsingString()}.{x},"))}} + _ when IgnoreExceptions => null, + _ => throw new global::System.ArgumentOutOfRangeException(nameof(value), value.ToString(), $"Cannot convert '{value.ToString()}' to enum type '{{enumType.ToDisplayString()}}'."), + }; + } + + /// + /// Converts the parsed value to the enum type. + /// + public {{enumType.ToUsingString()}}? To{{enumType.ToCommandTargetMethodName()}}() => _value; +} +"""; + } } file static class Extensions diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index c9ceac00..70241f49 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -168,6 +168,26 @@ public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) { return $"{commandObjectType.Name}Builder"; } + + public IEnumerable EnumerateEnumPropertyTypes() + { + var enums = new HashSet(SymbolEqualityComparer.Default); + foreach (var option in OptionProperties) + { + if (option.Type.TypeKind is TypeKind.Enum) + { + enums.Add(option.Type); + } + } + foreach (var value in PositionalArgumentProperties) + { + if (value.Type.TypeKind is TypeKind.Enum) + { + enums.Add(value.Type); + } + } + return enums; + } } internal abstract record PropertyGeneratingModel From e845c15ff59a02571980e4d9832d7338d5b4527e Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 15 Sep 2025 23:49:57 +0800 Subject: [PATCH 019/193] =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=8E=BB=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 4 ++-- .../ModelProviding/CommandModelProvider.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 99f23635..88f455b0 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -31,7 +31,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") .AddRawMembers(model.OptionProperties.Select(GenerateOptionPropertyCode)) - .AddRawMembers(model.PositionalArgumentProperties.Select(GenerateOptionPropertyCode)) + .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateOptionPropertyCode)) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", @@ -53,7 +53,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod m => m .BeginBracketScope("switch (propertyIndex)", l => l .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode)) - .AddRawStatements(model.PositionalArgumentProperties.Select(GenerateAssignPropertyValueCode)))) + .AddRawStatements(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode)))) .AddMethodDeclaration( $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", m => m diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 70241f49..389a910d 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -154,6 +154,18 @@ internal record CommandObjectGeneratingModel { } names => names.Count(x => x == ' ') + 1, }; + public IEnumerable EnumeratePositionalArgumentPropertiesExcludingSameNameOptions() + { + var optionNames = OptionProperties.Select(x => x.Type.Name).ToList(); + foreach (var positionalArgumentProperty in PositionalArgumentProperties) + { + if (!optionNames.Contains(positionalArgumentProperty.Type.Name, StringComparer.Ordinal)) + { + yield return positionalArgumentProperty; + } + } + } + public string? GetKebabCaseCommandNames() { if (CommandNames is not { } commandNames) From 89cf0e3481a5ac7f3ba5ab265acab0bd98b80cb4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 00:50:36 +0800 Subject: [PATCH 020/193] =?UTF-8?q?=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E4=BB=A5=E5=89=8D=E7=9A=84=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Program.cs | 50 --------- .../Generators/ModelBuilderGenerator.cs | 101 +++++++++--------- .../ModelProviding/CommandModelProvider.cs | 91 ++++++++++++---- .../CommandLineParsingOptions.cs | 48 +++++++++ .../GnuCommandLineParserTests.cs | 16 +-- 5 files changed, 178 insertions(+), 128 deletions(-) diff --git a/samples/DotNetCampus.CommandLine.Sample/Program.cs b/samples/DotNetCampus.CommandLine.Sample/Program.cs index f7c28fe1..524c73a0 100644 --- a/samples/DotNetCampus.CommandLine.Sample/Program.cs +++ b/samples/DotNetCampus.CommandLine.Sample/Program.cs @@ -107,10 +107,6 @@ private static void Run(string[] args) { Run4xInterceptor(args); } - else if (args[0] == "4.x-module") - { - Run4xModule(args); - } } [MethodImpl(MethodImplOptions.NoInlining)] @@ -130,52 +126,6 @@ private static void Run4xInterceptor(string[] args) { _ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void Run4xModule(string[] args) - { - Initialize(); - _ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - internal static void Initialize() - { - // DefaultOptions { CommandName = null } - global::DotNetCampus.Cli.CommandRunner.Register( - null, - global::DotNetCampus.Cli.DefaultOptionsBuilder.CreateInstance); - - // EditOptions { CommandName = "Edit" } - global::DotNetCampus.Cli.CommandRunner.Register( - "Edit", - global::DotNetCampus.Cli.Tests.Fakes.EditOptionsBuilder.CreateInstance); - - // Options { CommandName = null } - global::DotNetCampus.Cli.CommandRunner.Register( - null, - global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance); - - // PrintOptions { CommandName = "Print" } - global::DotNetCampus.Cli.CommandRunner.Register( - "Print", - global::DotNetCampus.Cli.Tests.Fakes.PrintOptionsBuilder.CreateInstance); - - // SampleCommandHandler { CommandName = "sample" } - global::DotNetCampus.Cli.CommandRunner.Register( - "sample", - global::DotNetCampus.Cli.SampleCommandHandlerBuilder.CreateInstance); - - // SampleOptions { CommandName = "sample-options" } - global::DotNetCampus.Cli.CommandRunner.Register( - "sample-options", - global::DotNetCampus.Cli.SampleOptionsBuilder.CreateInstance); - - // ShareOptions { CommandName = "Share" } - global::DotNetCampus.Cli.CommandRunner.Register( - "Share", - global::DotNetCampus.Cli.Tests.Fakes.ShareOptionsBuilder.CreateInstance); - } } // [CollectCommandHandlersFromThisAssembly] diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 88f455b0..c6b4576d 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -29,7 +29,11 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .Using("System") .Using("DotNetCampus.Cli.Compiler") .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t - .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") + .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") + .AddMethodDeclaration( + $"public static {model.CommandObjectType.ToUsingString()} CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine)", + m => m + .AddRawStatements($"return new {model.Namespace}.{model.GetBuilderTypeName()}(commandLine).Build();")) .AddRawMembers(model.OptionProperties.Select(GenerateOptionPropertyCode)) .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateOptionPropertyCode)) .AddRawText(GenerateBuildCode(model)) @@ -177,22 +181,29 @@ private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel """; } - private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGeneratingModel positionalArgumentPropertyGeneratingModel, int index, - int length) + private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGeneratingModel model, int index, int length) { - return length == 1 - ? $$""" + return length switch + { + 1 => $$""" if (argumentIndex is {{index}}) { - return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{positionalArgumentPropertyGeneratingModel.PropertyName}}", {{positionalArgumentPropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); } - """ - : $$""" + """, + _ when index + length <= 0 => $$""" + if (argumentIndex >= {{index}}) + { + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + } + """, + _ => $$""" if (argumentIndex is >= {{index}} and < {{index + length}}) { - return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{positionalArgumentPropertyGeneratingModel.PropertyName}}", {{positionalArgumentPropertyGeneratingModel.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); } - """; + """, + }; } private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) @@ -219,19 +230,6 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) { - // 对于不同的属性类型,生成不同的代码。 - // init: 属性要求必须立即赋值 - // nullable: 属性允许为 null - // cli: 实际命令行参数是否传入 - - // | init | nullable | cli | 行为 | - // | ---- | -------- | --- | ---------- | - // | 1 | 1 | 0 | 默认值 | - // | 1 | 0 | 0 | 抛异常 | - // | 0 | 1 | 0 | 保留初值 | - // | 0 | 0 | 0 | 保留初值 | - // | _ | _ | 1 | 赋值 | - var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); @@ -245,7 +243,7 @@ private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) {{( initRawArgumentsProperties.Count is 0 ? " // There is no [RawArguments] property to be initialized." - : string.Join("\n", initRawArgumentsProperties.Select(GenerateInitRawArgumentProperty)) + : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentProperty)) )}} // 2. [Option] @@ -267,7 +265,7 @@ initPositionalArgumentProperties.Count is 0 {{( setRawArgumentsProperties.Count is 0 ? "// There is no [RawArguments] property to be assigned." - : string.Join("\n", setRawArgumentsProperties.Select(GenerateSetRawArgumentProperty)) + : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentProperty)) )}} // 2. [Option] @@ -290,17 +288,34 @@ setPositionalArgumentProperties.Count is 0 private string GenerateInitProperty(PropertyGeneratingModel model) { + // 对于不同的属性类型,生成不同的代码。 + // init: 属性要求必须立即赋值 + // nullable: 属性允许为 null + // list: 属性是一个集合 + // cli: 实际命令行参数是否传入 + + // | init | nullable | list | cli | 行为 | + // | ---- | -------- | ---- | --- | ---------- | + // | 1 | 1 | _ | 0 | null | + // | 1 | 0 | 1 | 0 | 空集合 | + // | 1 | 0 | 0 | 0 | 抛异常 | + // | 0 | _ | _ | 0 | 保留初值 | + // | _ | _ | _ | 1 | 赋值 | + var toTarget = model.Type.ToCommandTargetMethodName(); - var fallback = model.IsNullable - ? " ?? null" - : model switch + var fallback = (model.IsNullable, model.Type.AsCommandPropertyType() is CommandPropertyType.List or CommandPropertyType.Dictionary) switch + { + (true, _) => " ?? null", + (false, true) => "", + (false, false) => model switch { OptionPropertyGeneratingModel option => $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", PositionalArgumentPropertyGeneratingModel positionalArgument => $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", _ => "", - }; + }, + }; return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){fallback},"; } @@ -322,14 +337,12 @@ private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex """; } - private string GenerateInitRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) - { - return $" {model.PropertyName} = commandLine.CommandLineArguments,"; - } - - private string GenerateSetRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) + private string GenerateRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) { - return $"result.{model.PropertyName} = commandLine.CommandLineArguments;"; + var assignment = $"{model.PropertyName} = (commandLine.CommandLineArguments as {model.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments]"; + return model.IsRequired || model.IsInitOnly + ? $" {assignment}," + : $"result.{assignment};"; } private string GenerateEnumDeclarationCode(ITypeSymbol enumType) @@ -378,19 +391,3 @@ public void Assign(ReadOnlySpan value) """; } } - -file static class Extensions -{ - public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol type) - { - return $"__GeneratedEnumArgument__{type.ToDisplayString().Replace('.', '_')}__"; - } - - public static string ToOptionValueTypeName(this CommandPropertyType type) => type switch - { - CommandPropertyType.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", - CommandPropertyType.List => "global::DotNetCampus.Cli.OptionValueType.List", - CommandPropertyType.Dictionary => "global::DotNetCampus.Cli.OptionValueType.Dictionary", - _ => "global::DotNetCampus.Cli.OptionValueType.Normal", - }; -} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 389a910d..e241057d 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -156,10 +156,10 @@ internal record CommandObjectGeneratingModel public IEnumerable EnumeratePositionalArgumentPropertiesExcludingSameNameOptions() { - var optionNames = OptionProperties.Select(x => x.Type.Name).ToList(); + var optionNames = OptionProperties.Select(x => x.PropertyName).ToList(); foreach (var positionalArgumentProperty in PositionalArgumentProperties) { - if (!optionNames.Contains(positionalArgumentProperty.Type.Name, StringComparer.Ordinal)) + if (!optionNames.Contains(positionalArgumentProperty.PropertyName, StringComparer.Ordinal)) { yield return positionalArgumentProperty; } @@ -297,16 +297,14 @@ public IReadOnlyList GetShortNames() return null; } - if (optionAttribute.ConstructorArguments.Length is 0) - { - // 必须至少有一个构造函数参数。 - return null; - } - List shortNames = []; List longNames = []; - if (optionAttribute.ConstructorArguments.Length is 1) + if (optionAttribute.ConstructorArguments.Length is 0) + { + // 没有构造函数参数时,不设置任何名称。 + } + else if (optionAttribute.ConstructorArguments.Length is 1) { // 只有一个构造函数参数时,要么是短名称(一定是字符),要么是长名称(一定是字符串)。 var arg = optionAttribute.ConstructorArguments[0]; @@ -474,10 +472,58 @@ internal static class CommandModelExtensions kindOptions: SymbolDisplayKindOptions.None ); - public static string ToCommandTargetMethodName(this ITypeSymbol type) + private static readonly SymbolDisplayFormat NotNullDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.None, + kindOptions: SymbolDisplayKindOptions.None, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNotNullableReferenceTypeModifier); + + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + { + string typeName; + + if (symbol is { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) + { + typeName = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType + // 获取 Nullable 中的 T。 + ? namedType.TypeArguments[0].ToDisplayString() + // 处理直接带有可空标记的类型 (int? 这种形式)。 + : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + } + else + { + typeName = symbol.ToDisplayString(); + } + + return $"__GeneratedEnumArgument__{typeName.Replace('.', '_')}__"; + } + + public static string ToOptionValueTypeName(this CommandPropertyType type) => type switch { + CommandPropertyType.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", + CommandPropertyType.List => "global::DotNetCampus.Cli.OptionValueType.List", + CommandPropertyType.Dictionary => "global::DotNetCampus.Cli.OptionValueType.Dictionary", + _ => "global::DotNetCampus.Cli.OptionValueType.Normal", + }; + + public static string ToCommandTargetMethodName(this ITypeSymbol typeSymbol) + { + if (typeSymbol.Kind is SymbolKind.ArrayType) + { + return "Array"; + } + + var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); + if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) + { + // Nullable 类型 + var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return ToCommandTargetMethodName(genericType); + } + // 取出类型的 .NET 类名称,不含泛型。如 bool 返回 Boolean,Dictionary 返回 Dictionary。 - return type.ToDisplayString(ToTargetTypeFormat) switch + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch { "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" or "IImmutableSet" or "IImmutableList" => "List", @@ -508,11 +554,6 @@ SpecialType.System_Double or return CommandPropertyType.Number; } - if (typeSymbol.SpecialType is SpecialType.System_Array) - { - return CommandPropertyType.List; - } - if (typeSymbol.TypeKind is TypeKind.Enum) { return CommandPropertyType.Enum; @@ -523,14 +564,27 @@ SpecialType.System_Double or return CommandPropertyType.String; } + if (typeSymbol.Kind is SymbolKind.ArrayType) + { + return CommandPropertyType.List; + } + + var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); + if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) + { + // Nullable 类型 + var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return AsCommandPropertyType(genericType); + } + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch { "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" or "IImmutableSet" or "IImmutableList" => CommandPropertyType.List, "ImmutableArray" or "List" or "ImmutableHashSet" or "Collection" or "HashSet" => CommandPropertyType.List, "IDictionary" or "IReadOnlyDictionary" => CommandPropertyType.Dictionary, - "ImmutableDictionary" or "Dictionary" => CommandPropertyType.Dictionary, - _ => CommandPropertyType.String, + "ImmutableDictionary" or "Dictionary" or "KeyValuePair" => CommandPropertyType.Dictionary, + _ => CommandPropertyType.Unknown, }; } } @@ -543,6 +597,7 @@ internal enum CommandPropertyType String, List, Dictionary, + Unknown, } file static class Extensions diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index d723d68f..aedd01b6 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -101,6 +101,54 @@ public readonly record struct CommandLineParsingOptions /// 详细设置命令行解析时的各种细节。 /// public CommandLineStyleDetails Style { get; init; } + + /// + /// 此命令行解析器支持从 Web 打开本地应用时传入的参数。
+ /// 此属性指定用于 URI 协议注册的方案名(scheme name)。 + ///
+ /// + /// + /// 例如:sample://open?url=DotNetCampus%20is%20a%20great%20team
+ /// 这里的 "sample" 就是方案名。
+ /// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 + ///
+ /// /// + /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
+ ///
+ /// 详细规则:
+ /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
+ /// 2. 参数部分以问号(?)开始,后面是键值对
+ /// 3. 多个参数之间用(&)符号分隔
+ /// 4. 每个参数的键值之间用等号(=)分隔
+ /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
+ /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
+ /// 7. 支持无值参数,被视为布尔值true,如?enabled
+ /// 8. 参数值为空字符串时保留等号,如?name=
+ /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
+ /// 10. 但在某些情况下,路径的前几个部分可能会被当作命令(含子命令),例如 myapp://open/file.txt 中,open 可能是命令,file.txt 是位置参数。具体解释为位置参数还是命令取决于应用的命令行处理器实现
+ /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
+ ///
+ /// + /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) + /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 + /// myapp://user/profile?id=123&tab=info # 带层级路径 + /// sample://document/edit?id=42&mode=full # 多参数和路径组合 + /// + /// # 特殊字符与编码 + /// yourapp://search?q=hello%20world # 编码空格 + /// myapp://open?query=C%23%20programming # 特殊字符编码 + /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) + /// + /// # 无值和空值参数 + /// myapp://settings?debug # 无值参数(视为true) + /// yourapp://profile?name=&id=123 # 空字符串值 + /// + /// # 路径与命令示例 + /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 + /// myapp://open/file.txt?temporary=true # open 是命令,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 + /// + ///
+ public IReadOnlyList? SchemeNames { get; init; } } /// diff --git a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs index 1a671486..d6bea3e4 100644 --- a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs @@ -276,7 +276,7 @@ public void QuotedArrayOption_ValueAssigned() { // Arrange string[] args = ["--files", "\"file with spaces.txt\"", "--files", "normal.txt", "--files", "\"another file.txt\""]; - string[]? files = null; // Act + string[]? files = null; // Act CommandLine.Parse(args, GNU) .AddHandler(o => { @@ -296,7 +296,7 @@ public void QuotedArrayWithEquals_ValueAssigned() { // Arrange string[] args = ["--paths=\"path with spaces\",regular-path,\"another path\""]; - string[]? paths = null; // Act + string[]? paths = null; // Act CommandLine.Parse(args, GNU) .AddHandler(o => { @@ -369,7 +369,7 @@ public void CaseSensitive_CorrectOptionParsed() string? upperValue = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = true }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) .AddHandler(o => { lowerValue = o.CaseSensitive; @@ -390,7 +390,7 @@ public void CaseInsensitive_CorrectOptionParsed() string? value = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = false }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = false } }) .AddHandler(o => value = o.IgnoreCase) .Run(); @@ -429,7 +429,7 @@ public void OptionCaseInsensitive_OverridesGlobalSensitive() string? option2Value = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = true }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) .AddHandler(o => { option1Value = o.OptionOne; @@ -450,7 +450,7 @@ public void GlobalCaseSensitive_DefaultOption_NotMatched() string? globalSensitiveValue = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = true }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) .AddHandler(o => { globalSensitiveValue = o.GlobalSensitive; @@ -469,7 +469,7 @@ public void OptionCaseSensitive_CaseMismatch_NotMatched() string? localSensitiveValue = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = true }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) .AddHandler(o => { localSensitiveValue = o.LocalSensitive; @@ -488,7 +488,7 @@ public void OptionCaseInsensitive_GlobalSensitive_StillMatched() string? localInsensitiveValue = null; // Act - CommandLine.Parse(args, GNU with { CaseSensitive = true }) + CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) .AddHandler(o => { localInsensitiveValue = o.LocalInsensitive; From 43092c9e8a0a34ee743eb2888273908de20e64c2 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 00:56:55 +0800 Subject: [PATCH 021/193] =?UTF-8?q?=E6=B6=88=E9=99=A4=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index 7db6cbdb..425b8ca7 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -179,8 +179,6 @@ public static IAsyncCommandRunnerBuilder AddHandlers(this ICoreCommandRunnerB where T : notnull, ICommandHandlerCollection, new() { throw new NotImplementedException(); - return builder.GetOrCreateRunner() - .AddHandlers(); } /// From 3f9aad2aa62626eb7ce2da5a982887e4471d9c18 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 07:53:46 +0800 Subject: [PATCH 022/193] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=BE=85=E5=8A=A9?= =?UTF-8?q?=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=E7=BC=96=E5=86=99=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExperimentalTests.cs | 169 ------------------ 1 file changed, 169 deletions(-) delete mode 100644 tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs b/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs deleted file mode 100644 index 48fc5ad6..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/ExperimentalTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Utils.Parsers; - -namespace DotNetCampus.Cli.Tests; - -internal class ExperimentalTests -{ - public void Test() - { - var foo = CommandLine.Parse( - ["--bool", "--number:42", "--string", "hello", "--strings", "one", "two", "three", "--dict", "key1=value"], - CommandLineParsingOptions.DotNet) - .As(); - } -} - -internal record Foo -{ - [Option("bool")] - public required bool BooleanProperty { get; init; } - - [Option("number")] - public required int NumberProperty { get; init; } - - [Option("string")] - public required string StringProperty { get; init; } - - [Option("strings")] - public required IReadOnlyList StringsProperty { get; init; } - - [Option("dict")] - public required IReadOnlyDictionary DictionaryProperty { get; init; } - - [Option("log-level")] - public required LogLevel LogLevelProperty { get; init; } -} - -internal sealed class ExperimentalFooBuilder(CommandLine commandLine) -{ - private BooleanArgument BooleanProperty { get; } - private NumberArgument NumberProperty { get; } - private StringArgument StringProperty { get; } - private StringListArgument StringsProperty { get; } - private DictionaryArgument DictionaryProperty { get; } - private __GeneratedEnumPropertyAssignment__LogLevel__ LogLevelProperty { get; } - - public Foo Build() - { - var parser = new CommandLineParser(commandLine, "Foo", 0) - { - MatchLongOption = MatchLongOption, - MatchShortOption = MatchShortOption, - MatchPositionalArguments = MatchPositionalArguments, - AssignPropertyValue = AssignPropertyValue, - }; - parser.Parse(); - return BuildCore(); - } - - private OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy) - { - // 先原样匹配一遍。 - if (namingPolicy.SupportsOrdinal()) - { - var match = longOption switch - { - "boolean-property" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), - _ => OptionValueMatch.NotMatch, - }; - if (match != OptionValueMatch.NotMatch) - { - return match; - } - } - // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 - if (namingPolicy.SupportsCamelCase()) - { - var match = longOption switch - { - "boolean-property" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), - _ => OptionValueMatch.NotMatch, - }; - return match; - } - return OptionValueMatch.NotMatch; - } - - private OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) - { - var match = shortOption switch - { - "b" => new OptionValueMatch(nameof(BooleanProperty), 0, OptionValueType.Boolean), - _ => OptionValueMatch.NotMatch, - }; - return match; - } - - private PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) - { - if (argumentIndex is 0) - { - return new PositionalArgumentValueMatch(nameof(StringProperty), 2, PositionalArgumentValueType.Normal); - } - return PositionalArgumentValueMatch.NotMatch; - } - - private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) - { - _ = propertyIndex switch - { - 0 => BooleanProperty.Assign(value[0] == '1'), - 1 => NumberProperty.Assign(value), - 2 => StringProperty.Assign(value), - 3 => StringsProperty.Append(value), - 4 => DictionaryProperty.Append(key, value), - 5 => LogLevelProperty.SetValue(value), - _ => throw new ArgumentOutOfRangeException(nameof(propertyIndex), propertyIndex, null), - }; - } - - private Foo BuildCore() - { - var result = new Foo - { - BooleanProperty = BooleanProperty.ToBoolean() ?? throw new InvalidOperationException("BooleanProperty 未被赋值"), - NumberProperty = NumberProperty.ToInt32() ?? throw new InvalidOperationException("NumberProperty 未被赋值"), - StringProperty = StringProperty.ToString() ?? throw new InvalidOperationException("StringProperty 未被赋值"), - StringsProperty = StringsProperty.ToList() ?? throw new InvalidOperationException("StringsProperty 未被赋值"), - DictionaryProperty = DictionaryProperty.ToDictionary() ?? throw new InvalidOperationException("DictionaryProperty 未被赋值"), - LogLevelProperty = LogLevelProperty.ToEnum() ?? throw new InvalidOperationException("LogLevelProperty 未被赋值"), - }; - - // 1. [RawArguments] - // result.MainArgs = commandLine.CommandLineArguments; - - // 2. [Option] - // There is no option to be assigned. - - // 3. [Value] - // There is no positional argument to be assigned. - - return result; - } - - // ReSharper disable once InconsistentNaming - private struct __GeneratedEnumPropertyAssignment__LogLevel__ - { - private LogLevel? _value; - - public bool SetValue(ReadOnlySpan value) - { - _ = value switch - { - "0" or "Debug" or "debug" => _value = LogLevel.Debug, - "1" or "Info" or "info" => _value = LogLevel.Info, - "2" or "Warning" or "warning" => _value = LogLevel.Warning, - "3" or "Error" or "error" => _value = LogLevel.Error, - "4" or "Fatal" or "fatal" => _value = LogLevel.Critical, - _ => throw new ArgumentOutOfRangeException(nameof(value), value.ToString(), null), - }; - return true; - } - - public LogLevel? ToEnum() => _value; - } -} From 9994608e3ffef0ef0d5793bbb129219756604882 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 08:26:38 +0800 Subject: [PATCH 023/193] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=BA=90=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E6=B6=89=E5=8F=8A=E5=88=B0=E7=9A=84=E5=90=84?= =?UTF-8?q?=E7=A7=8D=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/BuilderGenerator.cs | 345 ------------ .../Generators/ModelBuilderGenerator.cs | 69 +-- .../ModelProviding/CommandModelProvider.cs | 520 +----------------- .../InterceptorModelProvider.cs | 1 + .../Models/AssemblyCommandsGeneratingModel.cs | 10 + .../Models/CommandObjectGeneratingModel.cs | 78 +++ .../Generators/Models/CommandValueKind.cs | 43 ++ .../Models/GeneratingModelExtensions.cs | 143 +++++ ...OptionalArgumentPropertyGeneratingModel.cs | 182 ++++++ ...sitionalArgumentPropertyGeneratingModel.cs | 41 ++ .../Models/PropertyGeneratingModel.cs | 30 + .../RawArgumentPropertyGeneratingModel.cs | 25 + 12 files changed, 611 insertions(+), 876 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/AssemblyCommandsGeneratingModel.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/OptionalArgumentPropertyGeneratingModel.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PositionalArgumentPropertyGeneratingModel.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PropertyGeneratingModel.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/Generators/Models/RawArgumentPropertyGeneratingModel.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs deleted file mode 100644 index bfc53e98..00000000 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs +++ /dev/null @@ -1,345 +0,0 @@ -// using System.Collections.Immutable; -// using DotNetCampus.CommandLine.Generators.ModelProviding; -// using DotNetCampus.CommandLine.Utils.CodeAnalysis; -// using Microsoft.CodeAnalysis; -// using Microsoft.CodeAnalysis.Diagnostics; -// -// namespace DotNetCampus.CommandLine.Generators; -// -// [Generator(LanguageNames.CSharp)] -// public class BuilderGenerator : IIncrementalGenerator -// { -// public void Initialize(IncrementalGeneratorInitializationContext context) -// { -// var analyzerConfigOptionsProvider = context.AnalyzerConfigOptionsProvider; -// var commandOptionsProvider = context.SelectCommandObjects(); -// var assemblyCommandsProvider = context.SelectAssemblyCommands(); -// -// context.RegisterSourceOutput( -// commandOptionsProvider, -// Execute); -// -// context.RegisterSourceOutput( -// assemblyCommandsProvider.Collect().Combine(commandOptionsProvider.Collect()).Combine(analyzerConfigOptionsProvider), -// Execute); -// } -// -// private void Execute(SourceProductionContext context, CommandObjectGeneratingModel model) -// { -// var code = GenerateCommandObjectCreatorCode(model); -// context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.cs", code); -// } -// -// private void Execute(SourceProductionContext context, -// ((ImmutableArray Left, ImmutableArray Right) Left, AnalyzerConfigOptionsProvider Right) -// args) -// { -// var ((assemblyCommandsGeneratingModels, commandOptionsGeneratingModels), analyzerConfigOptions) = args; -// commandOptionsGeneratingModels = [..commandOptionsGeneratingModels.OrderBy(x => x.GetBuilderTypeName())]; -// -// if (analyzerConfigOptions.GlobalOptions.TryGetValue("DotNetCampusCommandLineUseInterceptor", out var useInterceptor) -// && !useInterceptor) -// { -// var moduleInitializerCode = GenerateModuleInitializerCode(commandOptionsGeneratingModels); -// context.AddSource("CommandLine.Metadata/_ModuleInitializer.g.cs", moduleInitializerCode); -// } -// -// foreach (var assemblyCommandsGeneratingModel in assemblyCommandsGeneratingModels) -// { -// var code = GenerateAssemblyCommandHandlerCode(assemblyCommandsGeneratingModel, commandOptionsGeneratingModels); -// context.AddSource( -// $"CommandLine.Metadata/{assemblyCommandsGeneratingModel.Namespace}.{assemblyCommandsGeneratingModel.AssemblyCommandHandlerType.Name}.g.cs", -// code); -// } -// } -// -// private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) -// { -// // | required | nullable | cli | 行为 | -// // | -------- | -------- | --- | ---------- | -// // | 0 | 0 | 0 | 分析器警告 | -// // | 1 | 0 | 0 | 抛异常 | -// // | 0 | 1 | 0 | 默认值 | -// // | 1 | 1 | 0 | 抛异常 | -// // | 0 | 0 | 1 | 赋值 | -// // | 1 | 0 | 1 | 赋值 | -// // | 0 | 1 | 1 | 赋值 | -// // | 1 | 1 | 1 | 赋值 | -// -// var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); -// var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); -// var initValueProperties = model.ValueProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); -// var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); -// var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); -// var setValueProperties = model.ValueProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); -// return $$""" -// #nullable enable -// namespace {{model.Namespace}}; -// -// /// -// /// 辅助 生成命令行选项、子命令或处理函数的创建。 -// /// -// {{(model.IsPublic ? "public" : "internal")}} sealed class {{model.GetBuilderTypeName()}} -// { -// public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) -// { -// var caseSensitive = commandLine.DefaultCaseSensitive; -// var result = new {{model.CommandObjectType.ToGlobalDisplayString()}} -// { -// // 1. [RawArguments] -// {{(initRawArgumentsProperties.Length is 0 ? " // MainArgs = commandLine.CommandLineArguments," : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} -// -// // 2. [Option] -// {{(initOptionProperties.Length is 0 ? " // There is no option to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateOptionPropertyAssignment)))}} -// -// // 3. [Value] -// {{(initValueProperties.Length is 0 ? " // There is no positional argument to be initialized." : string.Join("\n", initValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} -// }; -// -// // 1. [RawArguments] -// {{(setRawArgumentsProperties.Length is 0 ? " // result.MainArgs = commandLine.CommandLineArguments;" : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} -// -// // 2. [Option] -// {{(setOptionProperties.Length is 0 ? " // There is no option to be assigned." : string.Join("\n", setOptionProperties.Select(GenerateOptionPropertyAssignment)))}} -// -// // 3. [Value] -// {{(setValueProperties.Length is 0 ? " // There is no positional argument to be assigned." : string.Join("\n", setValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} -// -// return result; -// } -// } -// -// """; -// } -// -// private string GenerateOptionPropertyAssignment(OptionPropertyGeneratingModel property, int modelIndex) -// { -// var isInitProperty = property.IsRequired || property.IsInitOnly; -// var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; -// var caseSensitive = property.CaseSensitive switch -// { -// true => ", true", -// false => ", false", -// null => "", -// }; -// var exception = property.IsRequired -// ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{property.GetDisplayCommandOption()}'. Command line: {{commandLine}}\", \"{property.PropertyName}\")" -// : (property.IsNullable, property.IsValueType) switch -// { -// (true, _) => "null", -// (false, true) => "default", -// (false, false) => "null!", -// }; -// -// var getters = property.GenerateAllNames( -// shortOption => $"""commandLine.GetShortOption("{shortOption}"{caseSensitive})""", -// longOption => $"""commandLine.GetOption("{longOption}"{caseSensitive})""", -// (caseSensitiveLongOption, ignoreCaseLongName) => -// $"""commandLine.GetOption(caseSensitive ? "{caseSensitiveLongOption}" : "{ignoreCaseLongName}"{caseSensitive})""", -// aliasOption => $"""commandLine.GetOption("{aliasOption}")""" -// ); -// -// return (isInitProperty, getters) switch -// { -// // [Option("OptionName")] -// // public required string PropertyName { get; init; } -// (true, { Count: 1 }) => $""" -// {property.PropertyName} = {getters[0]}{toMethod} ?? {exception}, -// """, -// // [Option('o', "OptionName")] -// // public required string PropertyName { get; init; } -// (true, _) => $""" -// {property.PropertyName} = ({string.Join("\n ?? ", getters)}){toMethod} -// ?? {exception}, -// """, -// // [Option("OptionName")] -// // public string PropertyName { get; set; } -// (false, { Count: 1 }) => $$""" -// if ({{getters[0]}}{{toMethod}} is { } o{{modelIndex}}) -// { -// result.{{property.PropertyName}} = o{{modelIndex}}; -// } -// """, -// // [Option('o', "OptionName")] -// // public string PropertyName { get; set; } -// (false, _) => $$""" -// if (({{string.Join("\n ?? ", getters)}}){{toMethod}} is { } o{{modelIndex}}) -// { -// result.{{property.PropertyName}} = o{{modelIndex}}; -// } -// """, -// }; -// } -// -// private string GenerateValuePropertyAssignment(CommandObjectGeneratingModel model, ValuePropertyGeneratingModel property, int modelIndex) -// { -// var toMethod = GetCommandLinePropertyValueToMethodName(property.Type) is { } tm ? $"?.{tm}()" : ""; -// var baseIndex = model.GetCommandLevel(); -// var indexLengthCode = (property.Index, property.Length) switch -// { -// (null, null) => $"{baseIndex}, 1", -// (null, { } length) when length == int.MaxValue => $"{baseIndex}, int.MaxValue", -// (null, { } length) => $"{baseIndex}, {length}", -// ({ } index, null) => $"{baseIndex + index}, 1", -// ({ } index, { } length) when length == int.MaxValue => $"{baseIndex + index}, int.MaxValue", -// ({ } index, { } length) => $"{baseIndex + index}, {length}", -// }; -// var exception = property.IsRequired -// ? $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at {property.Index ?? 0}. Command line: {{commandLine}}\", \"{property.PropertyName}\")" -// : (property.IsNullable, property.IsValueType) switch -// { -// (true, _) => "null", -// (false, true) => "default", -// (false, false) => "null!", -// }; -// if (property.IsRequired || property.IsInitOnly) -// { -// return $""" -// {property.PropertyName} = commandLine.GetPositionalArgument({$"{indexLengthCode}"}){toMethod} ?? {exception}, -// """; -// } -// else -// { -// return $$""" -// if (commandLine.GetPositionalArgument({{$"{indexLengthCode}"}}){{toMethod}} is { } p{{modelIndex}}) -// { -// result.{{property.PropertyName}} = p{{modelIndex}}; -// } -// """; -// } -// } -// -// private string GenerateRawArgumentsPropertyAssignment(RawArgumentsPropertyGeneratingModel property) -// { -// var isInitProperty = property.IsRequired || property.IsInitOnly; -// if (isInitProperty) -// { -// return $""" -// {property.PropertyName} = (commandLine.CommandLineArguments as {property.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments], -// """; -// } -// else -// { -// return $$""" -// result.{{property.PropertyName}} = (commandLine.CommandLineArguments as {{property.Type.ToDisplayString()}}) ?? [..commandLine.CommandLineArguments]; -// """; -// } -// } -// -// /// -// /// 获取一个方法名,调用该方法可使“命令行属性值”转换为“目标类型”。 -// /// -// /// 目标类型。 -// /// 方法名。 -// private string? GetCommandLinePropertyValueToMethodName(ITypeSymbol targetType) -// { -// // 特殊处理接口,因为接口不支持隐式转换,所以要调用专门的转换方法。 -// if (targetType.TypeKind is TypeKind.Interface) -// { -// return targetType.Name switch -// { -// "IEnumerable" or "IReadOnlyList" or "IList" or "ICollection" => "ToList", -// "IReadOnlyDictionary" or "IDictionary" => "ToDictionary", -// // 专门生成不存在的方法名和全名注释,编译不通过,同时还能辅助报告错误原因。 -// _ => $"To{targetType.Name}/* {targetType.ToDisplayString()} */", -// }; -// } -// -// // 特殊处理枚举和可空枚举,因为枚举类型不可穷举,所以要调用专门的转换方法。 -// if (targetType.ToDisplayString().EndsWith("?") && targetType.TypeKind is TypeKind.Struct) -// { -// // 拿到可空类型内部的类型,如 int? -> int。 -// targetType = ((INamedTypeSymbol)targetType).TypeArguments[0]; -// } -// if (targetType.TypeKind is TypeKind.Enum) -// { -// return $"ToEnum<{targetType.ToNotNullGlobalDisplayString()}>"; -// } -// -// // 其他类型使用隐式转换。 -// return null; -// } -// -// private string GenerateModuleInitializerCode(ImmutableArray models) -// { -// return $$""" -// #nullable enable -// namespace DotNetCampus.Cli; -// -// /// -// /// 为本程序集中的所有命令行选项、子命令或处理函数编译时信息初始化。 -// /// -// internal static class CommandLineModuleInitializer -// { -// [global::System.Runtime.CompilerServices.ModuleInitializerAttribute] -// internal static void Initialize() -// { -// {{string.Join("\n\n", models.Select(GenerateCommandRunnerRegisterCode))}} -// } -// } -// -// """; -// } -// -// private string GenerateCommandRunnerRegisterCode(CommandObjectGeneratingModel model) -// { -// var commandCode = model.GetKebabCaseCommandNames() is { } vn ? $"\"{vn}\"" : "null"; -// return $$""" -// // {{model.CommandObjectType.Name}} { CommandName = {{commandCode}} } -// global::DotNetCampus.Cli.CommandRunner.Register<{{model.CommandObjectType.ToGlobalDisplayString()}}>( -// {{commandCode}}, -// global::{{model.Namespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); -// """; -// } -// -// private string GenerateAssemblyCommandHandlerCode(AssemblyCommandsGeneratingModel model, ImmutableArray models) -// { -// return $$""" -// #nullable enable -// namespace {{model.Namespace}}; -// -// #pragma warning disable CS0162 -// -// /// -// /// 提供一种辅助自动搜集并执行本程序集中所有命令行处理器的方式。 -// /// -// partial class {{model.AssemblyCommandHandlerType.Name}} : global::DotNetCampus.Cli.Utils.Handlers.GeneratedAssemblyCommandHandlerCollection -// { -// public {{model.AssemblyCommandHandlerType.Name}}() -// { -// {{string.Join("\n", models.GroupBy(x => x.GetKebabCaseCommandNames()).Select(GenerateAssemblyCommandHandlerMatchCode))}} -// } -// } -// -// """; -// } -// -// private string GenerateAssemblyCommandHandlerMatchCode(IGrouping group) -// { -// var models = group.ToList(); -// if (models.Count is 1) -// { -// var model = models[0]; -// if (model.IsHandler) -// { -// var assignment = group.Key is { } commandName ? $"Creators[\"{commandName}\"]" : "Default"; -// return $""" -// {assignment} = cl => (global::DotNetCampus.Cli.ICommandHandler)global::{model.Namespace}.{model.GetBuilderTypeName()}.CreateInstance(cl); -// """; -// } -// else -// { -// return $""" -// // 类型 {model.CommandObjectType.Name} 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 -// """; -// } -// } -// else -// { -// var commandName = group.Key is { } cn ? $"\"{cn}\"" : "null"; -// return $""" -// throw new global::DotNetCampus.Cli.Exceptions.CommandNameAmbiguityException($"Multiple command handlers match the same command name '{group.Key ?? "null"}': {string.Join(", ", models.Select(x => x.CommandObjectType.Name))}.", {commandName}); -// """; -// } -// } -// } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index c6b4576d..57fb5b2e 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -1,5 +1,6 @@ using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; +using DotNetCampus.CommandLine.Generators.Models; using Microsoft.CodeAnalysis; namespace DotNetCampus.CommandLine.Generators; @@ -67,14 +68,14 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod return builder.ToString(); } - private string GenerateOptionPropertyCode(PropertyGeneratingModel model) => model.Type.AsCommandPropertyType() switch + private string GenerateOptionPropertyCode(PropertyGeneratingModel model) => model.Type.AsCommandValueKind() switch { - CommandPropertyType.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", - CommandPropertyType.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", + CommandValueKind.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", _ => $"// 不支持解析类型为 {model.Type.ToDisplayString()} 的属性 {model.PropertyName}。", }; @@ -96,7 +97,7 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return optionProperties.Length is 0 + return optionProperties.Count is 0 ? "// 没有长名称选项,无需匹配。" : $$""" var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; @@ -114,7 +115,7 @@ private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) } """; - static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) + static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { if (names.Count == 0) { @@ -131,7 +132,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead return string.Join("\n", names.Select(name => $$""" if (longOption.Equals("{{name}}".AsSpan(), {{comparison}})) { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } """)); } @@ -140,7 +141,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return optionProperties.Length is 0 + return optionProperties.Count is 0 ? "// 没有短名称选项,无需匹配。" : $$""" var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; @@ -148,7 +149,7 @@ private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} """; - static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IReadOnlyList names) + static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { if (names.Count == 0) { @@ -165,7 +166,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead return string.Join("\n", names.Select(name => $$""" if (shortOption.Equals("{{name}}".AsSpan(), {{comparison}})) { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandPropertyType().ToOptionValueTypeName()}}); + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } """)); } @@ -174,7 +175,7 @@ static string GenerateOptionMatchCode(OptionPropertyGeneratingModel model, IRead private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel model) { var positionalArgumentProperties = model.PositionalArgumentProperties; - return positionalArgumentProperties.Length is 0 + return positionalArgumentProperties.Count is 0 ? "// 没有位置参数,无需匹配。" : $$""" {{string.Join("\n", positionalArgumentProperties.Select(x => GenerateMatchPositionalArgumentCode(x, x.Index, x.Length)))}} @@ -208,16 +209,16 @@ private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGen private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) { - var assign = model.Type.AsCommandPropertyType() switch + var assign = model.Type.AsCommandValueKind() switch { - CommandPropertyType.Boolean => $"{model.PropertyName}.Assign(value[0] == '1');", - CommandPropertyType.List => $"{model.PropertyName}.Append(value);", - CommandPropertyType.Dictionary => $"{model.PropertyName}.Append(key, value);", + CommandValueKind.Boolean => $"{model.PropertyName}.Assign(value[0] == '1');", + CommandValueKind.List => $"{model.PropertyName}.Append(value);", + CommandValueKind.Dictionary => $"{model.PropertyName}.Append(key, value);", _ => $"{model.PropertyName}.Assign(value);", }; var propertyIndex = model switch { - OptionPropertyGeneratingModel optionPropertyGeneratingModel => optionPropertyGeneratingModel.PropertyIndex, + OptionalArgumentPropertyGeneratingModel optionPropertyGeneratingModel => optionPropertyGeneratingModel.PropertyIndex, PositionalArgumentPropertyGeneratingModel positionalArgumentPropertyGeneratingModel => positionalArgumentPropertyGeneratingModel.PropertyIndex, _ => -1, }; @@ -230,12 +231,12 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) { - var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); - var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); - var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequired || x.IsInitOnly).ToList(); - var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); - var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); - var setPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToList(); + var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); + var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); + var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequiredOrInit).ToList(); + var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequiredOrInit).ToList(); + var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequiredOrInit).ToList(); + var setPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => !x.IsRequiredOrInit).ToList(); return $$""" var result = new {{model.CommandObjectType.ToUsingString()}} { @@ -302,14 +303,14 @@ private string GenerateInitProperty(PropertyGeneratingModel model) // | 0 | _ | _ | 0 | 保留初值 | // | _ | _ | _ | 1 | 赋值 | - var toTarget = model.Type.ToCommandTargetMethodName(); - var fallback = (model.IsNullable, model.Type.AsCommandPropertyType() is CommandPropertyType.List or CommandPropertyType.Dictionary) switch + var toTarget = model.Type.ToCommandValueNonAbstractName(); + var fallback = (model.IsNullable, model.Type.AsCommandValueKind() is CommandValueKind.List or CommandValueKind.Dictionary) switch { (true, _) => " ?? null", (false, true) => "", (false, false) => model switch { - OptionPropertyGeneratingModel option => + OptionalArgumentPropertyGeneratingModel option => $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", PositionalArgumentPropertyGeneratingModel positionalArgument => $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", @@ -321,11 +322,11 @@ private string GenerateInitProperty(PropertyGeneratingModel model) private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex) { - var toTarget = model.Type.ToCommandTargetMethodName(); + var toTarget = model.Type.ToCommandValueNonAbstractName(); var variablePrefix = model switch { - RawArgumentsPropertyGeneratingModel => "a", - OptionPropertyGeneratingModel => "o", + RawArgumentPropertyGeneratingModel => "a", + OptionalArgumentPropertyGeneratingModel => "o", PositionalArgumentPropertyGeneratingModel => "v", _ => "", }; @@ -337,10 +338,10 @@ private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex """; } - private string GenerateRawArgumentProperty(RawArgumentsPropertyGeneratingModel model) + private string GenerateRawArgumentProperty(RawArgumentPropertyGeneratingModel model) { var assignment = $"{model.PropertyName} = (commandLine.CommandLineArguments as {model.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments]"; - return model.IsRequired || model.IsInitOnly + return model.IsRequiredOrInit ? $" {assignment}," : $"result.{assignment};"; } @@ -386,7 +387,7 @@ public void Assign(ReadOnlySpan value) /// /// Converts the parsed value to the enum type. /// - public {{enumType.ToUsingString()}}? To{{enumType.ToCommandTargetMethodName()}}() => _value; + public {{enumType.ToUsingString()}}? To{{enumType.ToCommandValueNonAbstractName()}}() => _value; } """; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index e241057d..1bd313bb 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -1,6 +1,5 @@ -using System.Collections.Immutable; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Utils; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.CommandLine.Generators.Models; using DotNetCampus.CommandLine.Utils.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -55,15 +54,16 @@ public static IncrementalValuesProvider SelectComm ; // 4. 拥有 [Option] 特性的属性。 var optionProperties = typeSymbol - .GetAttributedProperties(OptionPropertyGeneratingModel.TryParse); + .GetAttributedProperties(OptionalArgumentPropertyGeneratingModel.TryParse); // 5. 拥有 [Value] 特性的属性。 var valueProperties = typeSymbol .GetAttributedProperties(PositionalArgumentPropertyGeneratingModel.TryParse); // 6. 拥有 [RawArguments] 特性的属性。 var rawArgumentsProperties = typeSymbol - .GetAttributedProperties(RawArgumentsPropertyGeneratingModel.TryParse); + .GetAttributedProperties(RawArgumentPropertyGeneratingModel.TryParse); - if (!isOptions && !isHandler && attribute is null && optionProperties.IsEmpty && valueProperties.IsEmpty && rawArgumentsProperties.IsEmpty) + if (!isOptions && !isHandler && attribute is null + && optionProperties.Count is 0 && valueProperties.Count is 0 && rawArgumentsProperties.Count is 0) { // 不是命令行选项类型。 return null; @@ -73,13 +73,13 @@ public static IncrementalValuesProvider SelectComm var commandNames = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString(); var isPublic = typeSymbol.DeclaredAccessibility == Accessibility.Public; - for (var i = 0; i < optionProperties.Length; i++) + for (var i = 0; i < optionProperties.Count; i++) { optionProperties[i].PropertyIndex = i; } - for (var i = 0; i < valueProperties.Length; i++) + for (var i = 0; i < valueProperties.Count; i++) { - valueProperties[i].PropertyIndex = i + optionProperties.Length; + valueProperties[i].PropertyIndex = i + optionProperties.Count; } return new CommandObjectGeneratingModel @@ -126,506 +126,32 @@ public static IncrementalValuesProvider SelectA } } -internal record CommandObjectGeneratingModel -{ - private static readonly ImmutableArray SupportedPostfixes = ["Options", "CommandOptions", "Handler", "CommandHandler", ""]; - - public required string Namespace { get; init; } - - public required INamedTypeSymbol CommandObjectType { get; init; } - - public required bool IsPublic { get; init; } - - public required string? CommandNames { get; init; } - - public required bool IsHandler { get; init; } - - public required ImmutableArray OptionProperties { get; init; } - - public required ImmutableArray PositionalArgumentProperties { get; init; } - - public required ImmutableArray RawArgumentsProperties { get; init; } - - public string GetBuilderTypeName() => GetBuilderTypeName(CommandObjectType); - - public int GetCommandLevel() => CommandNames switch - { - null => 0, - { } names => names.Count(x => x == ' ') + 1, - }; - - public IEnumerable EnumeratePositionalArgumentPropertiesExcludingSameNameOptions() - { - var optionNames = OptionProperties.Select(x => x.PropertyName).ToList(); - foreach (var positionalArgumentProperty in PositionalArgumentProperties) - { - if (!optionNames.Contains(positionalArgumentProperty.PropertyName, StringComparer.Ordinal)) - { - yield return positionalArgumentProperty; - } - } - } - - public string? GetKebabCaseCommandNames() - { - if (CommandNames is not { } commandNames) - { - return null; - } - return string.Join(" ", commandNames.Split([' '], StringSplitOptions.RemoveEmptyEntries) - .Select(x => NamingHelper.MakeKebabCase(x, false, false))); - } - - public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) - { - return $"{commandObjectType.Name}Builder"; - } - - public IEnumerable EnumerateEnumPropertyTypes() - { - var enums = new HashSet(SymbolEqualityComparer.Default); - foreach (var option in OptionProperties) - { - if (option.Type.TypeKind is TypeKind.Enum) - { - enums.Add(option.Type); - } - } - foreach (var value in PositionalArgumentProperties) - { - if (value.Type.TypeKind is TypeKind.Enum) - { - enums.Add(value.Type); - } - } - return enums; - } -} - -internal abstract record PropertyGeneratingModel -{ - public required string PropertyName { get; init; } - - public required ITypeSymbol Type { get; init; } - - public required bool IsRequired { get; init; } - - public required bool IsInitOnly { get; init; } - - public required bool IsNullable { get; init; } -} - -internal record OptionPropertyGeneratingModel : PropertyGeneratingModel -{ - public required bool IsValueType { get; init; } - - public required IReadOnlyList ShortNames { get; init; } - - public required IReadOnlyList LongNames { get; init; } - - public required bool? CaseSensitive { get; init; } - - public int PropertyIndex { get; set; } = -1; - - /// - /// 返回开发者定义的长选项名称列表,按定义顺序返回。
- /// 如果没有定义,则返回 kebab-case 风格的属性名作为默认名称; - /// 如果有定义,无论定义了什么,都视其为 kebab-case 风格的名称。 - ///
- public IReadOnlyList GetOrdinalLongNames() - { - List list = []; - if (LongNames.Count is 0) - { - list.Add(NamingHelper.MakeKebabCase(PropertyName)); - } - else - { - foreach (var longName in LongNames) - { - if (!string.IsNullOrEmpty(longName) && !list.Contains(longName, StringComparer.Ordinal)) - { - list.Add(longName); - } - } - } - return list; - } - - public IReadOnlyList GetPascalCaseLongNames() - { - List list = []; - if (LongNames.Count is 0) - { - list.Add(PropertyName); - } - else - { - foreach (var longName in LongNames) - { - if (!string.IsNullOrEmpty(longName)) - { - var pascalCase = NamingHelper.MakePascalCase(longName); - if (!list.Contains(pascalCase, StringComparer.Ordinal)) - { - list.Add(pascalCase); - } - } - } - } - return list; - } - - public IReadOnlyList GetShortNames() - { - List list = []; - foreach (var shortName in ShortNames) - { - if (!string.IsNullOrEmpty(shortName) && !list.Contains(shortName, StringComparer.Ordinal)) - { - list.Add(shortName); - } - } - return list; - } - - public static OptionPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) - { - var optionAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); - if (optionAttribute is null) - { - return null; - } - - List shortNames = []; - List longNames = []; - - if (optionAttribute.ConstructorArguments.Length is 0) - { - // 没有构造函数参数时,不设置任何名称。 - } - else if (optionAttribute.ConstructorArguments.Length is 1) - { - // 只有一个构造函数参数时,要么是短名称(一定是字符),要么是长名称(一定是字符串)。 - var arg = optionAttribute.ConstructorArguments[0]; - if (arg.Type?.SpecialType is SpecialType.System_Char) - { - var shortName = arg.Value?.ToString(); - if (!string.IsNullOrEmpty(shortName)) - { - shortNames.Add(shortName!); - } - } - else if (arg.Type?.SpecialType is SpecialType.System_String) - { - var longName = arg.Value?.ToString(); - if (!string.IsNullOrEmpty(longName)) - { - longNames.Add(longName!); - } - } - } - else if (optionAttribute.ConstructorArguments.Length is 2) - { - // 有两个构造函数参数时,第一个参数是短名称(字符、字符串、字符串数组),第二个参数是长名称(字符串、字符串数组)。 - var shortArg = optionAttribute.ConstructorArguments[0]; - if (shortArg.Type?.SpecialType is SpecialType.System_Char) - { - var shortName = shortArg.Value?.ToString(); - if (!string.IsNullOrEmpty(shortName)) - { - shortNames.Add(shortName!); - } - } - else if (shortArg.Type?.SpecialType is SpecialType.System_String) - { - var shortName = shortArg.Value?.ToString(); - if (!string.IsNullOrEmpty(shortName)) - { - shortNames.Add(shortName!); - } - } - else if (shortArg.Kind is TypedConstantKind.Array) - { - foreach (var value in shortArg.Values) - { - var shortName = value.Value?.ToString(); - if (!string.IsNullOrEmpty(shortName) && !shortNames.Contains(shortName, StringComparer.Ordinal)) - { - shortNames.Add(shortName!); - } - } - } - var longArg = optionAttribute.ConstructorArguments[1]; - if (longArg.Type?.SpecialType is SpecialType.System_String) - { - var longName = longArg.Value?.ToString(); - if (!string.IsNullOrEmpty(longName)) - { - longNames.Add(longName!); - } - } - else if (longArg.Kind is TypedConstantKind.Array) - { - foreach (var value in longArg.Values) - { - var longName = value.Value?.ToString(); - if (!string.IsNullOrEmpty(longName) && !longNames.Contains(longName, StringComparer.Ordinal)) - { - longNames.Add(longName!); - } - } - } - } - - var caseSensitive = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.CaseSensitive)).Value.Value?.ToString(); - - return new OptionPropertyGeneratingModel - { - PropertyName = propertySymbol.Name, - Type = propertySymbol.Type, - IsRequired = propertySymbol.IsRequired, - IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, - IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, - IsValueType = propertySymbol.Type.IsValueType, - ShortNames = shortNames, - LongNames = longNames, - CaseSensitive = caseSensitive is not null && bool.TryParse(caseSensitive, out var result) ? result : null, - }; - } -} - -internal record PositionalArgumentPropertyGeneratingModel : PropertyGeneratingModel -{ - public required bool IsValueType { get; init; } - - public required int Index { get; init; } - - public required int Length { get; init; } - - public int PropertyIndex { get; set; } = -1; - - public static PositionalArgumentPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) - { - var valueAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); - if (valueAttribute is null) - { - return null; - } - - var index = valueAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); - var length = - // 优先从命名属性中拿。 - valueAttribute.NamedArguments - .FirstOrDefault(a => a.Key == nameof(ValueAttribute.Length)).Value.Value?.ToString() - // 其次从构造函数参数中拿。 - ?? valueAttribute.ConstructorArguments.ElementAtOrDefault(1).Value?.ToString(); - - return new PositionalArgumentPropertyGeneratingModel - { - PropertyName = propertySymbol.Name, - Type = propertySymbol.Type, - IsRequired = propertySymbol.IsRequired, - IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, - IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, - IsValueType = propertySymbol.Type.IsValueType, - Index = index is not null && int.TryParse(index, out var result) ? result : 0, - Length = length is not null && int.TryParse(length, out var result2) ? result2 : 1, - }; - } -} - -internal record RawArgumentsPropertyGeneratingModel : PropertyGeneratingModel -{ - public static RawArgumentsPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) - { - var rawArgumentsAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); - if (rawArgumentsAttribute is null) - { - return null; - } - - return new RawArgumentsPropertyGeneratingModel - { - PropertyName = propertySymbol.Name, - Type = propertySymbol.Type, - IsRequired = propertySymbol.IsRequired, - IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, - IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, - }; - } -} - -internal record AssemblyCommandsGeneratingModel -{ - public required string Namespace { get; init; } - - public required INamedTypeSymbol AssemblyCommandHandlerType { get; init; } -} - -internal static class CommandModelExtensions -{ - private static readonly SymbolDisplayFormat ToTargetTypeFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.None, - kindOptions: SymbolDisplayKindOptions.None - ); - - private static readonly SymbolDisplayFormat NotNullDisplayFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.None, - kindOptions: SymbolDisplayKindOptions.None, - miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNotNullableReferenceTypeModifier); - - public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) - { - string typeName; - - if (symbol is { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) - { - typeName = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType - // 获取 Nullable 中的 T。 - ? namedType.TypeArguments[0].ToDisplayString() - // 处理直接带有可空标记的类型 (int? 这种形式)。 - : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); - } - else - { - typeName = symbol.ToDisplayString(); - } - - return $"__GeneratedEnumArgument__{typeName.Replace('.', '_')}__"; - } - - public static string ToOptionValueTypeName(this CommandPropertyType type) => type switch - { - CommandPropertyType.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", - CommandPropertyType.List => "global::DotNetCampus.Cli.OptionValueType.List", - CommandPropertyType.Dictionary => "global::DotNetCampus.Cli.OptionValueType.Dictionary", - _ => "global::DotNetCampus.Cli.OptionValueType.Normal", - }; - - public static string ToCommandTargetMethodName(this ITypeSymbol typeSymbol) - { - if (typeSymbol.Kind is SymbolKind.ArrayType) - { - return "Array"; - } - - var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); - if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) - { - // Nullable 类型 - var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return ToCommandTargetMethodName(genericType); - } - - // 取出类型的 .NET 类名称,不含泛型。如 bool 返回 Boolean,Dictionary 返回 Dictionary。 - return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch - { - "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" - or "IImmutableSet" or "IImmutableList" => "List", - "IDictionary" or "IReadOnlyDictionary" => "Dictionary", - var name => name, - }; - } - - public static CommandPropertyType AsCommandPropertyType(this ITypeSymbol typeSymbol) - { - if (typeSymbol.SpecialType is SpecialType.System_Boolean) - { - return CommandPropertyType.Boolean; - } - - if (typeSymbol.SpecialType is SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_Int32 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Single or - SpecialType.System_Double or - SpecialType.System_Decimal) - { - return CommandPropertyType.Number; - } - - if (typeSymbol.TypeKind is TypeKind.Enum) - { - return CommandPropertyType.Enum; - } - - if (typeSymbol.SpecialType is SpecialType.System_String) - { - return CommandPropertyType.String; - } - - if (typeSymbol.Kind is SymbolKind.ArrayType) - { - return CommandPropertyType.List; - } - - var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); - if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) - { - // Nullable 类型 - var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return AsCommandPropertyType(genericType); - } - - return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch - { - "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" - or "IImmutableSet" or "IImmutableList" => CommandPropertyType.List, - "ImmutableArray" or "List" or "ImmutableHashSet" or "Collection" or "HashSet" => CommandPropertyType.List, - "IDictionary" or "IReadOnlyDictionary" => CommandPropertyType.Dictionary, - "ImmutableDictionary" or "Dictionary" or "KeyValuePair" => CommandPropertyType.Dictionary, - _ => CommandPropertyType.Unknown, - }; - } -} - -internal enum CommandPropertyType -{ - Boolean, - Number, - Enum, - String, - List, - Dictionary, - Unknown, -} - file static class Extensions { - public static IEnumerable EnumerateBaseTypesRecursively(this ITypeSymbol type) - { - var current = type; - while (current != null) - { - yield return current; - current = current.BaseType; - } - } - - public static ImmutableArray GetAttributedProperties(this ITypeSymbol typeSymbol, + public static IReadOnlyList GetAttributedProperties(this ITypeSymbol typeSymbol, Func propertyParser) where TModel : class { return typeSymbol .EnumerateBaseTypesRecursively() // 递归获取所有基类 .Reverse() // (注意我们先给父类属性赋值,再给子类属性赋值) - .SelectMany(x => x.GetMembers()) // 的所有成员, - .OfType() // 然后取出属性, + .SelectMany(x => x.GetMembers()) // 的所有成员, + .OfType() // 然后取出属性, .Select(x => (PropertyName: x.Name, Model: propertyParser(x))) // 解析出 OptionPropertyGeneratingModel。 .Where(x => x.Model is not null) .GroupBy(x => x.PropertyName) // 按属性名去重。 .Select(x => x.Last().Model) // 随后,取子类的属性(去除父类的重名属性)。 .Cast() - .ToImmutableArray(); + .ToList(); + } + + private static IEnumerable EnumerateBaseTypesRecursively(this ITypeSymbol type) + { + var current = type; + while (current != null) + { + yield return current; + current = current.BaseType; + } } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs index ba9b865b..184bce32 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; +using DotNetCampus.CommandLine.Generators.Models; using DotNetCampus.CommandLine.Utils.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/AssemblyCommandsGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/AssemblyCommandsGeneratingModel.cs new file mode 100644 index 00000000..4461abc6 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/AssemblyCommandsGeneratingModel.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal record AssemblyCommandsGeneratingModel +{ + public required string Namespace { get; init; } + + public required INamedTypeSymbol AssemblyCommandHandlerType { get; init; } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs new file mode 100644 index 00000000..ad81ce6a --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -0,0 +1,78 @@ +using DotNetCampus.Cli.Utils; +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal record CommandObjectGeneratingModel +{ + public required string Namespace { get; init; } + + public required INamedTypeSymbol CommandObjectType { get; init; } + + public required bool IsPublic { get; init; } + + public required string? CommandNames { get; init; } + + public required bool IsHandler { get; init; } + + public required IReadOnlyList RawArgumentsProperties { get; init; } + + public required IReadOnlyList OptionProperties { get; init; } + + public required IReadOnlyList PositionalArgumentProperties { get; init; } + + public string GetBuilderTypeName() => GetBuilderTypeName(CommandObjectType); + + public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) + { + return $"{commandObjectType.Name}Builder"; + } + + public int GetCommandLevel() => CommandNames switch + { + null => 0, + { } names => names.Count(x => x == ' ') + 1, + }; + + public IEnumerable EnumeratePositionalArgumentPropertiesExcludingSameNameOptions() + { + var optionNames = OptionProperties.Select(x => x.PropertyName).ToList(); + foreach (var positionalArgumentProperty in PositionalArgumentProperties) + { + if (!optionNames.Contains(positionalArgumentProperty.PropertyName, StringComparer.Ordinal)) + { + yield return positionalArgumentProperty; + } + } + } + + public string? GetKebabCaseCommandNames() + { + if (CommandNames is not { } commandNames) + { + return null; + } + return string.Join(" ", commandNames.Split([' '], StringSplitOptions.RemoveEmptyEntries) + .Select(x => NamingHelper.MakeKebabCase(x, false, false))); + } + + public IEnumerable EnumerateEnumPropertyTypes() + { + var enums = new HashSet(SymbolEqualityComparer.Default); + foreach (var option in OptionProperties) + { + if (option.Type.TypeKind is TypeKind.Enum) + { + enums.Add(option.Type); + } + } + foreach (var value in PositionalArgumentProperties) + { + if (value.Type.TypeKind is TypeKind.Enum) + { + enums.Add(value.Type); + } + } + return enums; + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs new file mode 100644 index 00000000..bdf90d21 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs @@ -0,0 +1,43 @@ +namespace DotNetCampus.CommandLine.Generators.Models; + +/// +/// 从命令行解析参数的含义时,对于值(选项和位置参数)的类型的分类。
+/// 源生成器给命令行对象的不同类型属性赋值时,只有这些类型才存在代码上的差异,其他类型都可映射到这些类型上。 +///
+internal enum CommandValueKind +{ + /// + /// 尚不知道是什么类型。 + /// + Unknown, + + /// + /// 布尔类型。 + /// + Boolean, + + /// + /// 数值类型,包括所有的整数和浮点数。 + /// + Number, + + /// + /// 枚举类型。 + /// + Enum, + + /// + /// 字符串类型。 + /// + String, + + /// + /// 列表类型。 + /// + List, + + /// + /// 字典类型。 + /// + Dictionary, +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs new file mode 100644 index 00000000..eede7a9d --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs @@ -0,0 +1,143 @@ +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +/// +/// 为源生成器使用的数据模型提供扩展方法。 +/// +internal static class GeneratingModelExtensions +{ + private static readonly SymbolDisplayFormat ToTargetTypeFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.None, + kindOptions: SymbolDisplayKindOptions.None + ); + + /// + /// 假定 是一个命令行对象中一个枚举属性的属性类型, + /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, + /// 此方法返回这个辅助类型的名称。 + /// + /// 命令行对象中一个枚举属性的属性类型。 + /// 辅助类型的名称。 + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + { + string typeName; + + if (symbol is { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) + { + typeName = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType + // 获取 Nullable 中的 T。 + ? namedType.TypeArguments[0].ToDisplayString() + // 处理直接带有可空标记的类型 (int? 这种形式)。 + : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + } + else + { + typeName = symbol.ToDisplayString(); + } + + return $"__GeneratedEnumArgument__{typeName.Replace('.', '_')}__"; + } + + public static string ToCommandValueTypeName(this CommandValueKind type) => type switch + { + CommandValueKind.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", + CommandValueKind.List => "global::DotNetCampus.Cli.OptionValueType.List", + CommandValueKind.Dictionary => "global::DotNetCampus.Cli.OptionValueType.Dictionary", + _ => "global::DotNetCampus.Cli.OptionValueType.Normal", + }; + + /// + /// 获取类型的非抽象名称。
+ /// 对于命令行解析中所支持的各种接口,会被映射为其常见的具体类型名称。 + ///
+ /// 类型符号。 + /// 非抽象名称。 + public static string ToCommandValueNonAbstractName(this ITypeSymbol typeSymbol) + { + if (typeSymbol.Kind is SymbolKind.ArrayType) + { + return "Array"; + } + + var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); + if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) + { + // Nullable 类型 + var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return ToCommandValueNonAbstractName(genericType); + } + + // 取出类型的 .NET 类名称,不含泛型。如 bool 返回 Boolean,Dictionary 返回 Dictionary。 + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch + { + "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" + or "IImmutableSet" or "IImmutableList" => "List", + "IDictionary" or "IReadOnlyDictionary" => "Dictionary", + var name => name, + }; + } + + /// + /// 将类型符号映射为命令行值的种类。 + /// + /// 类型符号。 + /// 命令行值的种类。 + public static CommandValueKind AsCommandValueKind(this ITypeSymbol typeSymbol) + { + if (typeSymbol.SpecialType is SpecialType.System_Boolean) + { + return CommandValueKind.Boolean; + } + + if (typeSymbol.SpecialType is SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_Int32 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_Decimal) + { + return CommandValueKind.Number; + } + + if (typeSymbol.TypeKind is TypeKind.Enum) + { + return CommandValueKind.Enum; + } + + if (typeSymbol.SpecialType is SpecialType.System_String) + { + return CommandValueKind.String; + } + + if (typeSymbol.Kind is SymbolKind.ArrayType) + { + return CommandValueKind.List; + } + + var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); + if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) + { + // Nullable 类型 + var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return AsCommandValueKind(genericType); + } + + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch + { + "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" + or "IImmutableSet" or "IImmutableList" => CommandValueKind.List, + "ImmutableArray" or "List" or "ImmutableHashSet" or "Collection" or "HashSet" => CommandValueKind.List, + "IDictionary" or "IReadOnlyDictionary" => CommandValueKind.Dictionary, + "ImmutableDictionary" or "Dictionary" or "KeyValuePair" => CommandValueKind.Dictionary, + _ => CommandValueKind.Unknown, + }; + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/OptionalArgumentPropertyGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/OptionalArgumentPropertyGeneratingModel.cs new file mode 100644 index 00000000..360fe35c --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/OptionalArgumentPropertyGeneratingModel.cs @@ -0,0 +1,182 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Utils; +using DotNetCampus.CommandLine.Utils.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal sealed record OptionalArgumentPropertyGeneratingModel : PropertyGeneratingModel +{ + private OptionalArgumentPropertyGeneratingModel(IPropertySymbol propertySymbol) : base(propertySymbol) + { + } + + public required IReadOnlyList ShortNames { get; init; } + + public required IReadOnlyList LongNames { get; init; } + + public required bool? CaseSensitive { get; init; } + + public int PropertyIndex { get; set; } = -1; + + /// + /// 返回开发者定义的长选项名称列表,按定义顺序返回。
+ /// 如果没有定义,则返回 kebab-case 风格的属性名作为默认名称; + /// 如果有定义,无论定义了什么,都视其为 kebab-case 风格的名称。 + ///
+ public IReadOnlyList GetOrdinalLongNames() + { + List list = []; + if (LongNames.Count is 0) + { + list.Add(NamingHelper.MakeKebabCase(PropertyName)); + } + else + { + foreach (var longName in LongNames) + { + if (!string.IsNullOrEmpty(longName) && !list.Contains(longName, StringComparer.Ordinal)) + { + list.Add(longName); + } + } + } + return list; + } + + public IReadOnlyList GetPascalCaseLongNames() + { + List list = []; + if (LongNames.Count is 0) + { + list.Add(PropertyName); + } + else + { + foreach (var longName in LongNames) + { + if (!string.IsNullOrEmpty(longName)) + { + var pascalCase = NamingHelper.MakePascalCase(longName); + if (!list.Contains(pascalCase, StringComparer.Ordinal)) + { + list.Add(pascalCase); + } + } + } + } + return list; + } + + public IReadOnlyList GetShortNames() + { + List list = []; + foreach (var shortName in ShortNames) + { + if (!string.IsNullOrEmpty(shortName) && !list.Contains(shortName, StringComparer.Ordinal)) + { + list.Add(shortName); + } + } + return list; + } + + public static OptionalArgumentPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) + { + var optionAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); + if (optionAttribute is null) + { + return null; + } + + List shortNames = []; + List longNames = []; + + if (optionAttribute.ConstructorArguments.Length is 0) + { + // 没有构造函数参数时,不设置任何名称。 + } + else if (optionAttribute.ConstructorArguments.Length is 1) + { + // 只有一个构造函数参数时,要么是短名称(一定是字符),要么是长名称(一定是字符串)。 + var arg = optionAttribute.ConstructorArguments[0]; + if (arg.Type?.SpecialType is SpecialType.System_Char) + { + var shortName = arg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (arg.Type?.SpecialType is SpecialType.System_String) + { + var longName = arg.Value?.ToString(); + if (!string.IsNullOrEmpty(longName)) + { + longNames.Add(longName!); + } + } + } + else if (optionAttribute.ConstructorArguments.Length is 2) + { + // 有两个构造函数参数时,第一个参数是短名称(字符、字符串、字符串数组),第二个参数是长名称(字符串、字符串数组)。 + var shortArg = optionAttribute.ConstructorArguments[0]; + if (shortArg.Type?.SpecialType is SpecialType.System_Char) + { + var shortName = shortArg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (shortArg.Type?.SpecialType is SpecialType.System_String) + { + var shortName = shortArg.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName)) + { + shortNames.Add(shortName!); + } + } + else if (shortArg.Kind is TypedConstantKind.Array) + { + foreach (var value in shortArg.Values) + { + var shortName = value.Value?.ToString(); + if (!string.IsNullOrEmpty(shortName) && !shortNames.Contains(shortName, StringComparer.Ordinal)) + { + shortNames.Add(shortName!); + } + } + } + var longArg = optionAttribute.ConstructorArguments[1]; + if (longArg.Type?.SpecialType is SpecialType.System_String) + { + var longName = longArg.Value?.ToString(); + if (!string.IsNullOrEmpty(longName)) + { + longNames.Add(longName!); + } + } + else if (longArg.Kind is TypedConstantKind.Array) + { + foreach (var value in longArg.Values) + { + var longName = value.Value?.ToString(); + if (!string.IsNullOrEmpty(longName) && !longNames.Contains(longName, StringComparer.Ordinal)) + { + longNames.Add(longName!); + } + } + } + } + + var caseSensitive = optionAttribute.NamedArguments.FirstOrDefault(a => a.Key == nameof(OptionAttribute.CaseSensitive)).Value.Value?.ToString(); + + return new OptionalArgumentPropertyGeneratingModel(propertySymbol) + { + ShortNames = shortNames, + LongNames = longNames, + CaseSensitive = caseSensitive is not null && bool.TryParse(caseSensitive, out var result) ? result : null, + }; + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PositionalArgumentPropertyGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PositionalArgumentPropertyGeneratingModel.cs new file mode 100644 index 00000000..bd01913f --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PositionalArgumentPropertyGeneratingModel.cs @@ -0,0 +1,41 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.CommandLine.Utils.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal sealed record PositionalArgumentPropertyGeneratingModel : PropertyGeneratingModel +{ + private PositionalArgumentPropertyGeneratingModel(IPropertySymbol propertySymbol) : base(propertySymbol) + { + } + + public required int Index { get; init; } + + public required int Length { get; init; } + + public int PropertyIndex { get; set; } = -1; + + public static PositionalArgumentPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) + { + var valueAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); + if (valueAttribute is null) + { + return null; + } + + var index = valueAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); + var length = + // 优先从命名属性中拿。 + valueAttribute.NamedArguments + .FirstOrDefault(a => a.Key == nameof(ValueAttribute.Length)).Value.Value?.ToString() + // 其次从构造函数参数中拿。 + ?? valueAttribute.ConstructorArguments.ElementAtOrDefault(1).Value?.ToString(); + + return new PositionalArgumentPropertyGeneratingModel(propertySymbol) + { + Index = index is not null && int.TryParse(index, out var result) ? result : 0, + Length = length is not null && int.TryParse(length, out var result2) ? result2 : 1, + }; + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PropertyGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PropertyGeneratingModel.cs new file mode 100644 index 00000000..331e68e3 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/PropertyGeneratingModel.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal abstract record PropertyGeneratingModel +{ + protected PropertyGeneratingModel(IPropertySymbol property) + { + PropertyName = property.Name; + Type = property.Type; + IsRequired = property.IsRequired; + IsInitOnly = property.SetMethod?.IsInitOnly ?? false; + IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated; + IsValueType = property.Type.IsValueType; + } + + public string PropertyName { get; } + + public ITypeSymbol Type { get; } + + public bool IsRequired { get; } + + public bool IsInitOnly { get; } + + public bool IsRequiredOrInit => IsRequired || IsInitOnly; + + public bool IsNullable { get; } + + public bool IsValueType { get; } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/RawArgumentPropertyGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/RawArgumentPropertyGeneratingModel.cs new file mode 100644 index 00000000..52008398 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/RawArgumentPropertyGeneratingModel.cs @@ -0,0 +1,25 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.CommandLine.Utils.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.Generators.Models; + +internal sealed record RawArgumentPropertyGeneratingModel : PropertyGeneratingModel +{ + private RawArgumentPropertyGeneratingModel(IPropertySymbol propertySymbol) : base(propertySymbol) + { + } + + public static RawArgumentPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) + { + var rawArgumentsAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); + if (rawArgumentsAttribute is null) + { + return null; + } + + return new RawArgumentPropertyGeneratingModel(propertySymbol) + { + }; + } +} From 2d4dd5e3ef889980511e5097c7bff7863234fb08 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 09:42:29 +0800 Subject: [PATCH 024/193] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=9A=84=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 52 +++++--- .../InterceptorModelProvider.cs | 6 +- src/DotNetCampus.CommandLine/CommandLine.cs | 6 +- src/DotNetCampus.CommandLine/CommandRunner.cs | 118 +++++++++++------- .../CommandRunnerBuilderExtensions.cs | 51 ++++---- ...jectCreator.cs => CommandObjectFactory.cs} | 2 +- .../Compiler/NamingPolicyNameGroup.cs | 33 +++++ .../Utils/Handlers/TaskCommandHandler.cs | 16 +-- .../Utils/Parsers/CommandLineParser.cs | 2 + 9 files changed, 184 insertions(+), 102 deletions(-) rename src/DotNetCampus.CommandLine/Compiler/{CommandObjectCreator.cs => CommandObjectFactory.cs} (84%) create mode 100644 src/DotNetCampus.CommandLine/Compiler/NamingPolicyNameGroup.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index 1f4678ac..ae70dcf0 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -17,39 +17,50 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var commandLineAsProvider = context.SelectCommandLineAsProvider(); var commandRunnerAddHandlerProvider = context.SelectCommandBuilderAddHandlerProvider(); var commandRunnerAddHandlerCoreActionProvider = context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Action"); - var commandRunnerAddHandlerAsyncActionProvider = context.SelectCommandBuilderAddHandlerProvider("IAsyncCommandRunnerBuilder", "global::System.Action"); - var commandRunnerAddHandlerCoreFuncIntProvider = context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Func"); - var commandRunnerAddHandlerAsyncFuncIntProvider = context.SelectCommandBuilderAddHandlerProvider("IAsyncCommandRunnerBuilder", "global::System.Func"); - var commandRunnerAddHandlerCoreFuncTaskProvider = context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Func"); - var commandRunnerAddHandlerCoreFuncTaskIntProvider = context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Func>"); + var commandRunnerAddHandlerAsyncActionProvider = + context.SelectCommandBuilderAddHandlerProvider("IAsyncCommandRunnerBuilder", "global::System.Action"); + var commandRunnerAddHandlerCoreFuncIntProvider = + context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Func"); + var commandRunnerAddHandlerAsyncFuncIntProvider = + context.SelectCommandBuilderAddHandlerProvider("IAsyncCommandRunnerBuilder", "global::System.Func"); + var commandRunnerAddHandlerCoreFuncTaskProvider = + context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", "global::System.Func"); + var commandRunnerAddHandlerCoreFuncTaskIntProvider = context.SelectCommandBuilderAddHandlerProvider("ICoreCommandRunnerBuilder", + "global::System.Func>"); context.RegisterSourceOutput(commandLineAsProvider.Collect().Combine(analyzerConfigOptionsProvider), CommandLineAs); context.RegisterSourceOutput(commandRunnerAddHandlerProvider.Collect().Combine(analyzerConfigOptionsProvider), CommandRunnerAddHandler); // ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler) context.RegisterSourceOutput(commandRunnerAddHandlerCoreActionProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Action{T})", "ICoreCommandRunnerBuilder", "System.Action", "ICommandRunnerBuilder")); + CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Action{T})", "ICoreCommandRunnerBuilder", "System.Action", + "ICommandRunnerBuilder")); // IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler) context.RegisterSourceOutput(commandRunnerAddHandlerAsyncActionProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "IAsyncCommandRunnerBuilder.AddHandler(Action{T})", "IAsyncCommandRunnerBuilder", "System.Action", "IAsyncCommandRunnerBuilder")); + CommandRunnerAddHandlerAction(c, args, "IAsyncCommandRunnerBuilder.AddHandler(Action{T})", "IAsyncCommandRunnerBuilder", "System.Action", + "IAsyncCommandRunnerBuilder")); // ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) context.RegisterSourceOutput(commandRunnerAddHandlerCoreFuncIntProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,int})", "ICoreCommandRunnerBuilder", "System.Func", "ICommandRunnerBuilder")); + CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,int})", "ICoreCommandRunnerBuilder", "System.Func", + "ICommandRunnerBuilder")); // IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler) context.RegisterSourceOutput(commandRunnerAddHandlerAsyncFuncIntProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "IAsyncCommandRunnerBuilder.AddHandler(Func{T,int})", "IAsyncCommandRunnerBuilder", "System.Func", "IAsyncCommandRunnerBuilder")); + CommandRunnerAddHandlerAction(c, args, "IAsyncCommandRunnerBuilder.AddHandler(Func{T,int})", "IAsyncCommandRunnerBuilder", "System.Func", + "IAsyncCommandRunnerBuilder")); // IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) context.RegisterSourceOutput(commandRunnerAddHandlerCoreFuncTaskProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,Task})", "ICoreCommandRunnerBuilder", "System.Func", + CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,Task})", "ICoreCommandRunnerBuilder", + "System.Func", "IAsyncCommandRunnerBuilder")); // IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler) context.RegisterSourceOutput(commandRunnerAddHandlerCoreFuncTaskIntProvider.Collect().Combine(analyzerConfigOptionsProvider), (c, args) => - CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,Task{int}})", "ICoreCommandRunnerBuilder", "System.Func>", + CommandRunnerAddHandlerAction(c, args, "ICoreCommandRunnerBuilder.AddHandler(Func{T,Task{int}})", "ICoreCommandRunnerBuilder", + "System.Func>", "IAsyncCommandRunnerBuilder")); } @@ -70,7 +81,8 @@ private void CommandLineAs(SourceProductionContext context, (ImmutableArray /// CommandRunner.AddHandler ///
- private void CommandRunnerAddHandler(SourceProductionContext context, (ImmutableArray Left, AnalyzerConfigOptionsProvider Right) args) + private void CommandRunnerAddHandler(SourceProductionContext context, + (ImmutableArray Left, AnalyzerConfigOptionsProvider Right) args) { if (context.ToDictionary(args) is not { } modelGroups || modelGroups.Count is 0) { @@ -165,12 +177,13 @@ private string GenerateCommandBuilderAddHandlerCode(ImmutableArray(builder, {{(model.GetKebabCaseCommandNames() is { } cn ? $"\"{cn}\"" : "null")}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, {{model.ToNameGroup()}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); } """; } - private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray models, string parameterThisName, string parameterTypeFullName, string returnName) + private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray models, string parameterThisName, + string parameterTypeFullName, string returnName) { var model = models[0]; return $$""" @@ -184,7 +197,7 @@ private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray(builder, handler, {{(model.CommandNames is { } cn ? $"\"{cn}\"" : "\"\"")}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, {{model.ToNameGroup()}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); } """; } @@ -192,6 +205,15 @@ private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray>? ToDictionary( this SourceProductionContext context, (ImmutableArray Left, AnalyzerConfigOptionsProvider Right) args) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs index 184bce32..49b86465 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs @@ -101,7 +101,7 @@ public static IncrementalValuesProvider SelectMethod // 获取 [Command("xxx")] 或 [Verb("xxx")] 特性中的 xxx。 var commandAttribute = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()) #pragma warning disable CS0618 // 类型或成员已过时 - ?? symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()) + ?? symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()) #pragma warning restore CS0618 // 类型或成员已过时 ; var commandNames = commandAttribute?.ConstructorArguments.FirstOrDefault() is { Kind: TypedConstantKind.Primitive } commandArgument @@ -129,14 +129,14 @@ string InvocationInfo { public string GetBuilderTypeName() => CommandObjectGeneratingModel.GetBuilderTypeName(CommandObjectType); - public string? GetKebabCaseCommandNames() + public string? GetPascalCaseCommandNames() { if (CommandNames is not { } commandNames) { return null; } return string.Join(" ", commandNames.Split([' '], StringSplitOptions.RemoveEmptyEntries) - .Select(x => NamingHelper.MakeKebabCase(x, false, false))); + .Select(NamingHelper.MakePascalCase)); } internal static IEqualityComparer CommandObjectTypeEqualityComparer { get; } = diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index 0deef366..f426cbed 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -86,13 +86,13 @@ public static CommandLine Parse(string singleLineCommandLineArgs, CommandLinePar /// /// 尝试将命令行参数转换为指定类型的实例。 /// - /// 由拦截器传入的命令处理器创建方法。 + /// 由拦截器传入的命令处理器创建方法。 /// 要转换的类型。 /// 转换后的实例。 [Pure, EditorBrowsable(EditorBrowsableState.Never)] - public T As(CommandObjectCreator creator) where T : notnull + public T As(CommandObjectFactory factory) where T : notnull { - return (T)creator(this); + return (T)factory(this); } /// diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index 1b4ccf72..0c7b8917 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -10,12 +10,19 @@ namespace DotNetCampus.Cli; public class CommandRunner : ICommandRunnerBuilder, IAsyncCommandRunnerBuilder { private readonly CommandLine _commandLine; - - private readonly SortedList _creators = new(StringLengthDescendingComparer.CaseSensitive); + private readonly SortedList _factories; + private readonly bool _supportsOrdinal; + private readonly bool _supportsPascalCase; + private CommandObjectFactory? _defaultFactory; internal CommandRunner(CommandLine commandLine) { _commandLine = commandLine; + _factories = commandLine.DefaultCaseSensitive + ? new SortedList(StringLengthDescendingComparer.CaseSensitive) + : new SortedList(StringLengthDescendingComparer.CaseInsensitive); + _supportsOrdinal = commandLine.ParsingOptions.Style.NamingPolicy.SupportsOrdinal(); + _supportsPascalCase = commandLine.ParsingOptions.Style.NamingPolicy.SupportsPascalCase(); } /// @@ -27,76 +34,103 @@ internal CommandRunner(CommandLine commandLine) /// public Task RunAsync() { - var (possibleCommandNames, creator) = MatchCreator(); + var (possibleCommandNames, factory) = MatchCreator(); - if (creator is null) + if (factory is null) { throw new CommandNameNotFoundException( string.IsNullOrEmpty(possibleCommandNames) - ? "No default command handler found. Please ensure that a default command handler is registered correctly." - : $"No command handler found for command '{possibleCommandNames}'. Please ensure that the command handler is registered correctly.", + ? "No command handler found. Please ensure that at least one command handler is registered by AddHandler()." + : $"No command handler found for command '{possibleCommandNames}'. Please ensure that the command handler is registered by AddHandler().", possibleCommandNames); } - var handler = (ICommandHandler)creator(_commandLine); + var handler = (ICommandHandler)factory(_commandLine); return handler.RunAsync(); } - private (string PossibleCommandNames, CommandObjectCreator? Creator) MatchCreator() + private (string PossibleCommandNames, CommandObjectFactory? Creator) MatchCreator() { - if (_creators.Count is 0) + if (_factories.Count > 0) { - return ("", null); - } - - var maxLength = _creators.Keys[0].Length; - var header = _commandLine.GetHeader(maxLength); - var stringComparison = _commandLine.DefaultCaseSensitive - ? StringComparison.Ordinal - : StringComparison.OrdinalIgnoreCase; + var maxLength = _factories.Keys[0].Length; + var header = _commandLine.GetHeader(maxLength); + var stringComparison = _commandLine.DefaultCaseSensitive + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; - foreach (var (command, info) in _creators) - { - if (header.StartsWith(command, stringComparison) - || info.CommandAliases.Any(alias => header.StartsWith(alias, stringComparison))) + foreach (var (command, factory) in _factories) { - return (header, info.Creator); + if (header.StartsWith(command, stringComparison)) + { + return (header, factory); + } } } - return (header, null); + if (_defaultFactory is { } defaultFactory) + { + return ("", defaultFactory); + } + + return (_commandLine.GetHeader(1), null); } /// /// 添加一个命令处理器。 /// - /// 由拦截器传入的的命令处理器的命令, 表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 + /// 由拦截器传入的的命令处理器的命令, 表示此处理器没有命令名称。 + /// 由拦截器传入的命令处理器创建方法。 /// 返回一个命令处理器构建器。 [EditorBrowsable(EditorBrowsableState.Never)] - internal CommandRunner AddHandlerCore(string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases - ) + internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObjectFactory factory) { - var isAdded = _creators.TryAdd(command ?? "", new CommandObjectCreationInfo + if (_supportsOrdinal) { - Creator = creator, - CommandAliases = commandAliases ?? [], - }); - if (!isAdded) + if (command.Ordinal is { } ordinal && !string.IsNullOrWhiteSpace(ordinal)) + { + // 包含命令名称。 + var isAdded = _factories.TryAdd(ordinal, factory); + if (!isAdded) + { + throw new InvalidOperationException($"The command '{ordinal}' is already registered."); + } + } + else + { + // 不包含命令名称,表示这是默认命令。 + if (_defaultFactory is not null) + { + throw new InvalidOperationException("The default command handler is already registered."); + } + _defaultFactory = factory; + } + } + if (_supportsPascalCase) { - throw new InvalidOperationException($"The command '{command}' is already registered."); + if (command.PascalCase is { } ordinal && !string.IsNullOrWhiteSpace(ordinal)) + { + // 包含命令名称。 + var isAdded = _factories.TryAdd(ordinal, factory); + if (!isAdded && !_supportsOrdinal) + { + // 转换的名称,之后在仅用转换名称时才需要抛出异常;否则很可能前面已经添加了一个相同的名称。 + throw new InvalidOperationException($"The command '{ordinal}' is already registered."); + } + } + else + { + // 不包含命令名称,表示这是默认命令。 + if (_defaultFactory is not null && !_supportsOrdinal) + { + // 如果支持双命名法,则允许前面已经注册了一个默认命令。 + throw new InvalidOperationException("The default command handler is already registered."); + } + _defaultFactory = factory; + } } return this; } - - private readonly record struct CommandObjectCreationInfo - { - public required CommandObjectCreator Creator { get; init; } - - public required IReadOnlyList CommandAliases { get; init; } - } } file static class CommandRunnerExtensions diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index 425b8ca7..8e9ca453 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -75,77 +75,70 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// /// 命令行执行器构造的链式调用。 /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 + /// 由拦截器传入的命令处理器创建方法。 /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull, ICommandHandler { return builder.GetOrCreateRunner() - .AddHandlerCore(command, creator, commandAliases); + .AddHandlerCore(command, factory); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { return builder.GetOrCreateRunner() - .AddHandlerCore(command, cl => new AnonymousCommandHandler(cl, creator, handler), commandAliases); + .AddHandlerCore(command, cl => new AnonymousCommandHandler(cl, factory, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); + return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, factory); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { return builder.GetOrCreateRunner() - .AddHandlerCore(command, cl => new AnonymousInt32CommandHandler(cl, creator, handler), commandAliases); + .AddHandlerCore(command, cl => new AnonymousInt32CommandHandler(cl, factory, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { - return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, creator, commandAliases); + return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, factory); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { return builder.GetOrCreateRunner() - .AddHandlerCore(command, cl => new AnonymousTaskCommandHandler(cl, creator, handler), commandAliases); + .AddHandlerCore(command, cl => new AnonymousTaskCommandHandler(cl, factory, handler)); } /// @@ -154,19 +147,17 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令行执行器构造的链式调用。 /// 用于处理已解析的命令行参数的委托。 /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令的别名列表,由源生成器生成,用于根据不同的命令行风格生成不同的命名法名称。 + /// 由拦截器传入的命令处理器创建方法。 /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler, - string? command, CommandObjectCreator creator, - IReadOnlyList? commandAliases = null + NamingPolicyNameGroup command, CommandObjectFactory factory ) where T : notnull { return builder.GetOrCreateRunner() - .AddHandlerCore(command, cl => new AnonymousTaskInt32CommandHandler(cl, creator, handler), commandAliases); + .AddHandlerCore(command, cl => new AnonymousTaskInt32CommandHandler(cl, factory, handler)); } /// diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs b/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs similarity index 84% rename from src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs rename to src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs index 81e510b0..1f78660b 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs @@ -8,4 +8,4 @@ /// /// 从已解析的命令行参数创建命令数据模型或处理器的委托。 /// -public delegate object CommandObjectCreator(CommandLine commandLine); +public delegate object CommandObjectFactory(CommandLine commandLine); diff --git a/src/DotNetCampus.CommandLine/Compiler/NamingPolicyNameGroup.cs b/src/DotNetCampus.CommandLine/Compiler/NamingPolicyNameGroup.cs new file mode 100644 index 00000000..94a6e1ae --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/NamingPolicyNameGroup.cs @@ -0,0 +1,33 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 同一个名称的不同命名法表示。 +/// +public readonly record struct NamingPolicyNameGroup +{ + /// + /// 创建一个新的命名组。 + /// + /// + /// + public NamingPolicyNameGroup(string ordinal, string pascalCase) + { + Ordinal = ordinal; + PascalCase = pascalCase; + } + + /// + /// 原始名称(我们将视之为 kebab-case)。 + /// + public string? Ordinal { get; } + + /// + /// kebab-case 名称,与 相同。 + /// + public string? KebabCase => Ordinal; + + /// + /// PascalCase 名称,基于 转换而来。 + /// + public string? PascalCase { get; } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 0c2a682e..9ac361d6 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -22,7 +22,7 @@ public Task RunAsync() internal sealed class AnonymousCommandHandler( CommandLine commandLine, - CommandObjectCreator creator, + CommandObjectFactory factory, Action handler) : ICommandHandler where T : notnull { @@ -30,7 +30,7 @@ internal sealed class AnonymousCommandHandler( public Task RunAsync() { - _options ??= (T)creator(commandLine); + _options ??= (T)factory(commandLine); if (_options is null) { throw new InvalidOperationException($"No options of type {typeof(T)} were created."); @@ -42,7 +42,7 @@ public Task RunAsync() internal sealed class AnonymousInt32CommandHandler( CommandLine commandLine, - CommandObjectCreator creator, + CommandObjectFactory factory, Func handler) : ICommandHandler where T : notnull { @@ -50,7 +50,7 @@ internal sealed class AnonymousInt32CommandHandler( public Task RunAsync() { - _options ??= (T)creator(commandLine); + _options ??= (T)factory(commandLine); if (_options is null) { throw new InvalidOperationException($"No options of type {typeof(T)} were created."); @@ -62,7 +62,7 @@ public Task RunAsync() internal sealed class AnonymousTaskCommandHandler( CommandLine commandLine, - CommandObjectCreator creator, + CommandObjectFactory factory, Func handler) : ICommandHandler where T : notnull { @@ -70,7 +70,7 @@ internal sealed class AnonymousTaskCommandHandler( public async Task RunAsync() { - _options ??= (T)creator(commandLine); + _options ??= (T)factory(commandLine); if (_options is null) { throw new InvalidOperationException($"No options of type {typeof(T)} were created."); @@ -82,7 +82,7 @@ public async Task RunAsync() internal sealed class AnonymousTaskInt32CommandHandler( CommandLine commandLine, - CommandObjectCreator creator, + CommandObjectFactory factory, Func> handler) : ICommandHandler where T : notnull { @@ -90,7 +90,7 @@ internal sealed class AnonymousTaskInt32CommandHandler( public Task RunAsync() { - _options ??= (T)creator(commandLine); + _options ??= (T)factory(commandLine); if (_options is null) { throw new InvalidOperationException($"No options of type {typeof(T)} were created."); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 54b8c260..8e790a2f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -397,6 +397,8 @@ private bool ParseOptionAndPositionalArgumentRegion() return _lastType switch { + // 上一个是起点或命令,后面只能是新的选项或位置参数。 + Cat.Start or Cat.Command => ParseOptionOrPositionalArgument(), // 值已经被上一个选项消费掉了,必须是新的选项或位置参数。 Cat.PositionalArgument or Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue => ParseOptionOrPositionalArgument(), // 多个短选项,后面不允许带值。 From 42350ac03afaf1bf2fd2e589126d82a909c81863 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 10:22:54 +0800 Subject: [PATCH 025/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E4=B8=80=E4=BA=9B=E4=BD=8E=E7=BA=A7=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 31 ++--- .../CommandSeparatorChars.cs | 16 +-- .../Compiler/PropertyAssignments.cs | 118 +++++++++--------- .../Utils/Parsers/CommandLineParser.cs | 17 ++- 4 files changed, 102 insertions(+), 80 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 57fb5b2e..2b36c690 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -70,12 +70,12 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod private string GenerateOptionPropertyCode(PropertyGeneratingModel model) => model.Type.AsCommandValueKind() switch { - CommandValueKind.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} {{ get; }} = new();", - CommandValueKind.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} {{ get; }} = new();", - CommandValueKind.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} {{ get; }} = new();", - CommandValueKind.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} {{ get; }} = new();", - CommandValueKind.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} {{ get; }} = new();", - CommandValueKind.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} {{ get; }} = new();", + CommandValueKind.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} = new();", + CommandValueKind.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} = new();", + CommandValueKind.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} = new();", + CommandValueKind.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} = new();", + CommandValueKind.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} = new();", + CommandValueKind.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} = new();", _ => $"// 不支持解析类型为 {model.Type.ToDisplayString()} 的属性 {model.PropertyName}。", }; @@ -211,10 +211,10 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) { var assign = model.Type.AsCommandValueKind() switch { - CommandValueKind.Boolean => $"{model.PropertyName}.Assign(value[0] == '1');", - CommandValueKind.List => $"{model.PropertyName}.Append(value);", - CommandValueKind.Dictionary => $"{model.PropertyName}.Append(key, value);", - _ => $"{model.PropertyName}.Assign(value);", + CommandValueKind.Boolean => $"{model.PropertyName} = {model.PropertyName}.Assign(value[0] == '1');", + CommandValueKind.List => $"{model.PropertyName} = {model.PropertyName}.Append(value);", + CommandValueKind.Dictionary => $"{model.PropertyName} = {model.PropertyName}.Append(key, value);", + _ => $"{model.PropertyName} = {model.PropertyName}.Assign(value);", }; var propertyIndex = model switch { @@ -353,7 +353,7 @@ private string GenerateEnumDeclarationCode(ITypeSymbol enumType) /// /// Provides parsing and assignment for the enum type . /// -private struct {{enumType.GetGeneratedEnumArgumentTypeName()}} +private readonly record struct {{enumType.GetGeneratedEnumArgumentTypeName()}} { /// /// Indicates whether to ignore exceptions when parsing fails. @@ -363,31 +363,32 @@ private struct {{enumType.GetGeneratedEnumArgumentTypeName()}} /// /// Stores the parsed enum value. /// - private {{enumType.ToUsingString()}}? _value; + private {{enumType.ToUsingString()}}? Value { get; init; } /// /// Assigns a value when a command line input is parsed. /// /// The parsed string value. - public void Assign(ReadOnlySpan value) + public {{enumType.GetGeneratedEnumArgumentTypeName()}} Assign(ReadOnlySpan value) { Span lowerValue = stackalloc char[value.Length]; for (var i = 0; i < value.Length; i++) { lowerValue[i] = char.ToLowerInvariant(value[i]); } - _value = lowerValue switch + {{enumType.ToUsingString()}}? newValue = lowerValue switch { {{string.Join("\n ", enumNames.Select(x => $" \"{x.ToLowerInvariant()}\" => {enumType.ToUsingString()}.{x},"))}} _ when IgnoreExceptions => null, _ => throw new global::System.ArgumentOutOfRangeException(nameof(value), value.ToString(), $"Cannot convert '{value.ToString()}' to enum type '{{enumType.ToDisplayString()}}'."), }; + return this with { Value = newValue }; } /// /// Converts the parsed value to the enum type. /// - public {{enumType.ToUsingString()}}? To{{enumType.ToCommandValueNonAbstractName()}}() => _value; + public {{enumType.ToUsingString()}}? To{{enumType.ToCommandValueNonAbstractName()}}() => Value; } """; } diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs index f586171c..699d741f 100644 --- a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs @@ -15,9 +15,9 @@ namespace DotNetCampus.Cli; /// /// 最多支持 4 个分隔符字符。 /// - private readonly ushort _chars; + private readonly uint _chars; - private CommandSeparatorChars(ushort packedChars) + private CommandSeparatorChars(uint packedChars) { _chars = packedChars; } @@ -32,14 +32,14 @@ public void CopyTo(Span buffer, out int length) var packed = _chars; while (packed != 0) { - var c = (char)(packed & 0xF); + var c = (char)(packed & 0xFF); if (length < buffer.Length) { buffer[length] = c; } length++; - packed >>= 4; + packed >>= 8; } } @@ -52,9 +52,9 @@ public IEnumerator GetEnumerator() var packed = _chars; while (packed != 0) { - var c = (char)(packed & 0xF); + var c = (char)(packed & 0xFF); yield return c; - packed >>= 4; + packed >>= 8; } } @@ -75,7 +75,7 @@ public static CommandSeparatorChars Create(params ReadOnlySpan chars) throw new ArgumentOutOfRangeException(nameof(chars), "最多只能指定 4 个分隔符字符。"); } - ushort packed = 0; + uint packed = 0; for (var i = chars.Length - 1; i >= 0; i--) { var c = chars[i]; @@ -84,7 +84,7 @@ public static CommandSeparatorChars Create(params ReadOnlySpan chars) throw new ArgumentException("不支持 null 字符作为分隔符。", nameof(chars)); } - packed = (ushort)((packed << 4) | c); + packed = (packed << 8) | c; } return new CommandSeparatorChars(packed); diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index 97e2c35a..34dfb442 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -8,20 +8,20 @@ namespace DotNetCampus.Cli.Compiler; /// /// 专门解析来自命令行的布尔类型,并辅助赋值给属性。 /// -public struct BooleanArgument +public readonly record struct BooleanArgument { /// /// 存储解析到的布尔值。 /// - private bool? _value; + private bool? Value { get; init; } /// /// 当命令行直接或间接输入了一个布尔参数时,调用此方法赋值。 /// /// 解析到的布尔值。 - public void Assign(bool value) + public BooleanArgument Assign(bool value) { - _value = value; + return new BooleanArgument { Value = value }; } /// @@ -29,14 +29,14 @@ public void Assign(bool value) /// public bool? ToBoolean() { - return _value; + return Value; } } /// /// 专门解析来自命令行的数值类型,并辅助赋值给属性。 /// -public struct NumberArgument +public readonly record struct NumberArgument { /// /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 @@ -46,13 +46,13 @@ public struct NumberArgument /// /// 存储解析到的数值。 /// - private decimal? _value; + private decimal? Value { get; init; } /// /// 当命令行输入了一个数值参数时,调用此方法赋值。 /// /// 解析到的数值字符串。 - public void Assign(ReadOnlySpan value) + public NumberArgument Assign(ReadOnlySpan value) { if (decimal.TryParse(value #if !NETCOREAPP3_1_OR_GREATER @@ -60,84 +60,85 @@ public void Assign(ReadOnlySpan value) #endif , out var doubleValue)) { - _value = doubleValue; + return this with { Value = doubleValue }; } - else if (!IgnoreExceptions) + if (!IgnoreExceptions) { throw new FormatException($"无法将 \"{value.ToString()}\" 转换为数值。"); } + return this; } /// /// 将解析到的值转换为字节。 /// - public byte? ToByte() => (byte?)_value; + public byte? ToByte() => (byte?)Value; /// /// 将解析到的值转换为有符号字节。 /// - public sbyte? ToSByte() => (sbyte?)_value; + public sbyte? ToSByte() => (sbyte?)Value; /// /// 将解析到的值转换为高精度浮点数。 /// - public decimal? ToDecimal() => _value; + public decimal? ToDecimal() => Value; /// /// 将解析到的值转换为双精度浮点数。 /// - public double? ToDouble() => (double?)_value; + public double? ToDouble() => (double?)Value; /// /// 将解析到的值转换为单精度浮点数。 /// - public float? ToSingle() => (float?)_value; + public float? ToSingle() => (float?)Value; /// /// 将解析到的值转换为 32 位整数。 /// - public int? ToInt32() => (int?)_value; + public int? ToInt32() => (int?)Value; /// /// 将解析到的值转换为无符号 32 位整数。 /// - public uint? ToUInt32() => (uint?)_value; + public uint? ToUInt32() => (uint?)Value; /// /// 将解析到的值转换为指针大小的整数。 /// - public nint? ToIntPtr() => (nint?)_value; + public nint? ToIntPtr() => (nint?)Value; /// /// 将解析到的值转换为无符号指针大小的整数。 /// - public nuint? ToUIntPtr() => (nuint?)_value; + public nuint? ToUIntPtr() => (nuint?)Value; /// /// 将解析到的值转换为 64 位整数。 /// - public long? ToInt64() => (long?)_value; + public long? ToInt64() => (long?)Value; /// /// 将解析到的值转换为无符号 64 位整数。 /// - public ulong? ToUInt64() => (ulong?)_value; + public ulong? ToUInt64() => (ulong?)Value; /// /// 将解析到的值转换为 16 位整数。 /// - public short? ToInt16() => (short?)_value; + public short? ToInt16() => (short?)Value; /// /// 将解析到的值转换为无符号 16 位整数。 /// - public ushort? ToUInt16() => (ushort?)_value; + public ushort? ToUInt16() => (ushort?)Value; } /// /// 专门解析来自命令行的字符串类型,并辅助赋值给属性。 /// -public struct StringArgument +public readonly record struct StringArgument { /// /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 @@ -147,27 +148,27 @@ public struct StringArgument /// /// 存储解析到的字符串值。 /// - private string? _text; + private string? Value { get; init; } /// /// 当命令行输入了一个字符串参数时,调用此方法赋值。 /// /// 解析到的字符串值。 - public void Assign(ReadOnlySpan value) + public StringArgument Assign(ReadOnlySpan value) { - _text = value.ToString(); + return this with { Value = value.ToString() }; } /// /// 将解析到的值转换为字符。 /// /// 如果字符串长度为 1,则返回该字符;否则返回 null。 - public char? ToChar() => _text switch + public char? ToChar() => Value switch { null => null, - { Length: 1 } => _text[0], + { Length: 1 } => Value[0], _ when IgnoreExceptions => null, - _ => throw new FormatException($"无法将 \"{_text}\" 转换为字符,因为它的长度不为 1。"), + _ => throw new FormatException($"无法将 \"{Value}\" 转换为字符,因为它的长度不为 1。"), }; /// @@ -175,34 +176,36 @@ public void Assign(ReadOnlySpan value) /// public override string? ToString() { - return _text; + return Value; } } /// /// 专门解析来自命令行的字符串集合类型,并辅助赋值给属性。 /// -public struct StringListArgument +public readonly record struct StringListArgument { /// /// 存储解析到的字符串列表。 /// - private List? _list; + private List? Value { get; init; } /// /// 当命令行输入了一个字符串参数时,调用此方法追加值。 /// /// 解析到的字符串值。 - public void Append(ReadOnlySpan value) + public StringListArgument Append(ReadOnlySpan value) { - _list ??= []; - _list.Add(value.ToString()); + var list = Value; + list ??= []; + list.Add(value.ToString()); + return new StringListArgument { Value = list }; } /// /// 将解析到的值转换为字符串数组。 /// - public string[] ToArray() => _list switch + public string[] ToArray() => Value switch { null or { Count: 0 } => [], { } values => [..values], @@ -212,7 +215,7 @@ public void Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变数组。 /// - public ImmutableArray ToImmutableArray() => _list switch + public ImmutableArray ToImmutableArray() => Value switch { #if NET8_0_OR_GREATER null or { Count: 0 } => [], @@ -226,7 +229,7 @@ public void Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变哈希集合。 /// - public ImmutableHashSet ToImmutableHashSet() => _list switch + public ImmutableHashSet ToImmutableHashSet() => Value switch { #if NET8_0_OR_GREATER null or { Count: 0 } => [], @@ -242,7 +245,7 @@ public void Append(ReadOnlySpan value) /// /// 将解析到的值转换为集合。 /// - public Collection ToCollection() => _list switch + public Collection ToCollection() => Value switch { null or { Count: 0 } => [], { } values => [..values], @@ -251,7 +254,7 @@ public void Append(ReadOnlySpan value) /// /// 将解析到的值转换为列表。 /// - public List ToList() => _list switch + public List ToList() => Value switch { null or { Count: 0 } => [], { } values => values, @@ -261,22 +264,24 @@ public void Append(ReadOnlySpan value) /// /// 专门解析来自命令行的字典类型,并辅助赋值给属性。 /// -public struct StringDictionaryArgument +public readonly record struct StringDictionaryArgument { /// /// 存储解析到的字符串字典。 /// - private Dictionary _dictionary; + private Dictionary Value { get; init; } /// /// 当命令行输入了一个键值对参数时,调用此方法追加值。 /// /// 解析到的键。 /// 解析到的值。 - public void Append(ReadOnlySpan key, ReadOnlySpan value) + public StringDictionaryArgument Append(ReadOnlySpan key, ReadOnlySpan value) { - _dictionary ??= []; - _dictionary[key.ToString()] = value.ToString(); + var dictionary = Value; + dictionary ??= []; + dictionary[key.ToString()] = value.ToString(); + return new StringDictionaryArgument { Value = dictionary }; } /// @@ -284,17 +289,17 @@ public void Append(ReadOnlySpan key, ReadOnlySpan value) /// public KeyValuePair? ToKeyValuePair() { - if (_dictionary is null || _dictionary.Count == 0) + if (Value is null || Value.Count == 0) { return null; } - if (_dictionary.Count > 1) + if (Value.Count > 1) { throw new InvalidOperationException("字典包含多个元素,无法转换为 KeyValuePair。"); } - using var enumerator = _dictionary.GetEnumerator(); + using var enumerator = Value.GetEnumerator(); enumerator.MoveNext(); return enumerator.Current; } @@ -304,7 +309,7 @@ public void Append(ReadOnlySpan key, ReadOnlySpan value) /// public Dictionary ToDictionary() { - return _dictionary ?? []; + return Value ?? []; } } @@ -315,7 +320,7 @@ public Dictionary ToDictionary() /// 源生成器会为各个枚举生成专门的编译时类型来处理枚举的赋值。
/// 此类型是为那些在运行时才知道枚举类型的场景准备的。 /// -public struct RuntimeEnumArgument where T : unmanaged, Enum +public readonly record struct RuntimeEnumArgument where T : unmanaged, Enum { /// /// 指示在解析失败时是否忽略异常并保持未初始化的状态。 @@ -325,13 +330,13 @@ public struct RuntimeEnumArgument where T : unmanaged, Enum /// /// 存储解析到的枚举值。 /// - private T? _value; + private T? Value { get; init; } /// /// 当命令行输入了一个数值参数时,调用此方法赋值。 /// /// 解析到的数值字符串。 - public void Assign(ReadOnlySpan value) + public RuntimeEnumArgument Assign(ReadOnlySpan value) { if (Enum.TryParse(value #if !NET6_0_OR_GREATER @@ -339,16 +344,17 @@ public void Assign(ReadOnlySpan value) #endif , ignoreCase: true, out var enumValue)) { - _value = enumValue; + return this with { Value = enumValue }; } - else if (!IgnoreExceptions) + if (!IgnoreExceptions) { throw new FormatException($"无法将 \"{value.ToString()}\" 转换为 {typeof(T).FullName} 枚举。"); } + return this; } /// /// 将解析到的值转换为枚举。 /// - public T? ToEnum() => _value; + public T? ToEnum() => Value; } diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 8e790a2f..a198ce0e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -153,7 +153,22 @@ public CommandLineParsingResult Parse() } case Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue: { - AssignOptionValue(currentOption, value); + var optionMatch = state switch + { + Cat.LongOptionWithValue => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), + Cat.ShortOptionWithValue => MatchShortOption(optionName.Name, _caseSensitive), + _ => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy) switch + { + { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName.Name, _caseSensitive), + var t => t, + }, + }; + if (optionMatch.ValueType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + } + AssignOptionValue(optionMatch, value); break; } case Cat.ErrorOption: From c69a26ae609c3d740c16220af345c87f2188fb3c Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 11:20:16 +0800 Subject: [PATCH 026/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20nameof=EF=BC=8C?= =?UTF-8?q?=E8=BF=99=E6=A0=B7=E8=83=BD=E7=9F=A5=E9=81=93=E6=98=AF=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E5=90=8D=EF=BC=8C=E8=80=8C=E4=B8=8D=E6=98=AF=E6=9F=90?= =?UTF-8?q?=E4=B8=AA=E5=91=BD=E5=90=8D=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 2b36c690..5cf7364f 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -132,7 +132,7 @@ static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel mo return string.Join("\n", names.Select(name => $$""" if (longOption.Equals("{{name}}".AsSpan(), {{comparison}})) { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({{model.PropertyName}}), {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } """)); } @@ -166,7 +166,7 @@ static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel mo return string.Join("\n", names.Select(name => $$""" if (shortOption.Equals("{{name}}".AsSpan(), {{comparison}})) { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({{model.PropertyName}}), {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } """)); } From 0894492f073a8e667becfd225b983c1aaa0af139 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 13:16:32 +0800 Subject: [PATCH 027/193] =?UTF-8?q?=E4=BC=A0=E9=80=92=E5=B9=B6=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=A4=84=E7=90=86=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 4 +- src/DotNetCampus.CommandLine/CommandRunner.cs | 12 +- .../Compiler/PropertyAssignments.cs | 19 ++- .../Utils/Parsers/CommandLineParser.cs | 97 +++++++++---- .../Utils/Parsers/CommandLineParsingResult.cs | 127 ++++++++++++++++-- 5 files changed, 212 insertions(+), 47 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 5cf7364f..fce455eb 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -89,7 +89,7 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ MatchPositionalArguments = MatchPositionalArguments, AssignPropertyValue = AssignPropertyValue, }; - parser.Parse(); + parser.Parse().ThrowIfError(); return BuildCore(commandLine); } """; @@ -211,7 +211,7 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) { var assign = model.Type.AsCommandValueKind() switch { - CommandValueKind.Boolean => $"{model.PropertyName} = {model.PropertyName}.Assign(value[0] == '1');", + CommandValueKind.Boolean => $"{model.PropertyName} = {model.PropertyName}.Assign(value);", CommandValueKind.List => $"{model.PropertyName} = {model.PropertyName}.Append(value);", CommandValueKind.Dictionary => $"{model.PropertyName} = {model.PropertyName}.Append(key, value);", _ => $"{model.PropertyName} = {model.PropertyName}.Assign(value);", diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index 0c7b8917..e6404b98 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -93,7 +93,7 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje var isAdded = _factories.TryAdd(ordinal, factory); if (!isAdded) { - throw new InvalidOperationException($"The command '{ordinal}' is already registered."); + throw new CommandNameAmbiguityException($"The command '{ordinal}' is already registered.", ordinal); } } else @@ -101,21 +101,21 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje // 不包含命令名称,表示这是默认命令。 if (_defaultFactory is not null) { - throw new InvalidOperationException("The default command handler is already registered."); + throw new CommandNameAmbiguityException("The default command handler is already registered.", null); } _defaultFactory = factory; } } if (_supportsPascalCase) { - if (command.PascalCase is { } ordinal && !string.IsNullOrWhiteSpace(ordinal)) + if (command.PascalCase is { } pascal && !string.IsNullOrWhiteSpace(pascal)) { // 包含命令名称。 - var isAdded = _factories.TryAdd(ordinal, factory); + var isAdded = _factories.TryAdd(pascal, factory); if (!isAdded && !_supportsOrdinal) { // 转换的名称,之后在仅用转换名称时才需要抛出异常;否则很可能前面已经添加了一个相同的名称。 - throw new InvalidOperationException($"The command '{ordinal}' is already registered."); + throw new CommandNameAmbiguityException($"The command '{pascal}' is already registered.", pascal); } } else @@ -124,7 +124,7 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje if (_defaultFactory is not null && !_supportsOrdinal) { // 如果支持双命名法,则允许前面已经注册了一个默认命令。 - throw new InvalidOperationException("The default command handler is already registered."); + throw new CommandNameAmbiguityException("The default command handler is already registered.", null); } _defaultFactory = factory; } diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index 34dfb442..90dc42e3 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -2,12 +2,14 @@ using System.Collections.Immutable; #endif using System.Collections.ObjectModel; +using System.Diagnostics; namespace DotNetCampus.Cli.Compiler; /// /// 专门解析来自命令行的布尔类型,并辅助赋值给属性。 /// +[DebuggerDisplay("Boolean: {Value,nq}")] public readonly record struct BooleanArgument { /// @@ -19,9 +21,18 @@ public readonly record struct BooleanArgument /// 当命令行直接或间接输入了一个布尔参数时,调用此方法赋值。 /// /// 解析到的布尔值。 - public BooleanArgument Assign(bool value) + public BooleanArgument Assign(ReadOnlySpan value) { - return new BooleanArgument { Value = value }; + return value switch + { + // 因为解析器已经保证了布尔参数只可能出现以下三种值: + // - []: 表示 true + // - ['1', ..]: 表示 true + // - ['0', ..]: 表示 false + [] => new BooleanArgument { Value = true }, + ['1', ..] => new BooleanArgument { Value = true }, + _ => new BooleanArgument { Value = false }, + }; } /// @@ -36,6 +47,7 @@ public BooleanArgument Assign(bool value) /// /// 专门解析来自命令行的数值类型,并辅助赋值给属性。 /// +[DebuggerDisplay("Number: {Value,nq}")] public readonly record struct NumberArgument { /// @@ -138,6 +150,7 @@ public NumberArgument Assign(ReadOnlySpan value) /// /// 专门解析来自命令行的字符串类型,并辅助赋值给属性。 /// +[DebuggerDisplay("String: {Value,nq}")] public readonly record struct StringArgument { /// @@ -183,6 +196,7 @@ public StringArgument Assign(ReadOnlySpan value) /// /// 专门解析来自命令行的字符串集合类型,并辅助赋值给属性。 /// +[DebuggerDisplay("String: {Value,nq}")] public readonly record struct StringListArgument { /// @@ -264,6 +278,7 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 专门解析来自命令行的字典类型,并辅助赋值给属性。 /// +[DebuggerDisplay("String: {Value,nq}")] public readonly record struct StringDictionaryArgument { /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index a198ce0e..dcf0f2f0 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -89,6 +89,7 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int /// 命令行参数解析结果。 public CommandLineParsingResult Parse() { + var result = CommandLineParsingResult.Success; var arguments = _commandLine.CommandLineArguments; var currentOption = OptionValueMatch.NotMatch; var currentPositionArgumentIndex = 0; @@ -123,14 +124,19 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName.Name); + } + if (optionMatch.ValueType is OptionValueType.Boolean) + { + // 布尔选项必须立即赋值,因为后面是不一定需要跟值的。 + result = AssignOptionValue(optionMatch, []).Combine(result); } currentOption = optionMatch; break; } case Cat.OptionValue: { - AssignOptionValue(currentOption, value); + result = AssignOptionValue(currentOption, value).Combine(result); if (currentOption.ValueType is not OptionValueType.List) { // 如果不是集合,那么此选项已经结束。 @@ -166,15 +172,15 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, optionName.Name); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName.Name); } - AssignOptionValue(optionMatch, value); + result = AssignOptionValue(optionMatch, value).Combine(result); break; } case Cat.ErrorOption: { // 如果当前参数疑似选项但解析失败,则报告错误。 - return CommandLineParsingResult.OptionParseError(_commandLine, index); + return CommandLineParsingResult.OptionalArgumentParseError(_commandLine, index); } case Cat.MultiShortOptions: { @@ -186,7 +192,7 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is not OptionValueType.NotExist) { // 是一个多字符短选项。 - AssignOptionValue(optionMatch, []); + result = AssignOptionValue(optionMatch, []).Combine(result); break; } } @@ -198,9 +204,9 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, n); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, n); } - AssignOptionValue(optionMatch, []); + result = AssignOptionValue(optionMatch, []).Combine(result); } break; } @@ -214,7 +220,7 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is not OptionValueType.NotExist) { // 是一个多字符短选项。 - AssignOptionValue(optionMatch, []); + result = AssignOptionValue(optionMatch, []).Combine(result); break; } } @@ -226,9 +232,9 @@ public CommandLineParsingResult Parse() if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionNotFound(_commandLine, index, _commandObjectName, o); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, o); } - AssignOptionValue(optionMatch, v); + result = AssignOptionValue(optionMatch, v).Combine(result); } break; } @@ -236,11 +242,12 @@ public CommandLineParsingResult Parse() } } - return CommandLineParsingResult.Success; + return result; } - private void AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) + private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) { + var result = CommandLineParsingResult.Success; if (match.ValueType is OptionValueType.List) { Span separators = stackalloc char[4]; @@ -279,25 +286,44 @@ private void AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) if (index < 0) { // 剩余部分没有分隔符,全部作为一个值。 - SplitKeyValue(value[start..], out var k, out var v); + result = SplitKeyValue(value[start..], out var k, out var v).Combine(result); AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); break; } if (index > 0) { // 截取分隔符前的部分作为一个值。 - SplitKeyValue(value.Slice(start, index), out var k, out var v); + result = SplitKeyValue(value.Slice(start, index), out var k, out var v).Combine(result); AssignPropertyValue(match.PropertyName, match.PropertyIndex, k, v); } // 跳过分隔符,继续处理后续部分。 start += index + 1; } } + else if (match.ValueType is OptionValueType.Boolean) + { + var booleanValue = ParseBoolean(value); + if (booleanValue is null) + { + result = CommandLineParsingResult.BooleanValueParseError(_commandLine, value).Combine(result); + } + ReadOnlySpan finalValue = booleanValue switch + { + // 用户输入明确指定为 true。 + true => ['1'], + // 用户输入明确指定为 false。 + false => ['0'], + // 无法识别。 + _ => ['0'], + }; + AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], finalValue); + } else { // 普通值。 AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value); } + return result; } private void AssignPositionalArgumentValue(PositionalArgumentValueMatch match, ReadOnlySpan value) @@ -305,7 +331,7 @@ private void AssignPositionalArgumentValue(PositionalArgumentValueMatch match, R AssignPropertyValue(match.PropertyName, match.PropertyIndex, [], value); } - private static void SplitKeyValue(ReadOnlySpan item, + private CommandLineParsingResult SplitKeyValue(ReadOnlySpan item, out ReadOnlySpan key, out ReadOnlySpan value) { // 截至目前,所有的字典类型都使用 key=value 形式,如果将来新增的风格有其他符号,我们再用一样的分隔符方式来配置。 @@ -314,10 +340,33 @@ private static void SplitKeyValue(ReadOnlySpan item, { key = item; value = []; - return; + return CommandLineParsingResult.DictionaryValueParseError(_commandLine, item); } key = item[..index]; value = item[(index + 1)..]; + return CommandLineParsingResult.Success; + } + + internal static bool? ParseBoolean(ReadOnlySpan value) + { + if (value.Length <= 4 && ( + value.Equals("true".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("yes".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("on".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("1".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Length is 0)) + { + return true; + } + if (value.Length is > 0 and <= 5 && ( + value.Equals("false".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("no".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("off".AsSpan(), StringComparison.OrdinalIgnoreCase) || + value.Equals("0".AsSpan(), StringComparison.OrdinalIgnoreCase))) + { + return false; + } + return null; } } @@ -633,22 +682,14 @@ private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan a private bool ParseBooleanOptionValueOrNewOptionOrPositionalArgument() { var argument = _argument; - if (argument.Length <= 4 && ( - argument.Equals("true", StringComparison.OrdinalIgnoreCase) || - argument.Equals("yes", StringComparison.OrdinalIgnoreCase) || - argument.Equals("on", StringComparison.OrdinalIgnoreCase) || - argument.Equals("1", StringComparison.OrdinalIgnoreCase) || - argument is "")) + var booleanValue = CommandLineParser.ParseBoolean(argument.AsSpan()); + if (booleanValue is true) { Type = Cat.OptionValue; Value = "1".AsSpan(); return true; } - if (argument.Length is > 0 and <= 5 && ( - argument.Equals("false", StringComparison.OrdinalIgnoreCase) || - argument.Equals("no", StringComparison.OrdinalIgnoreCase) || - argument.Equals("off", StringComparison.OrdinalIgnoreCase) || - argument.Equals("0", StringComparison.OrdinalIgnoreCase))) + if (booleanValue is false) { Type = Cat.OptionValue; Value = "0".AsSpan(); diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs index a244561e..6d0ae50c 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -1,20 +1,69 @@ +using DotNetCampus.Cli.Exceptions; + namespace DotNetCampus.Cli.Utils.Parsers; /// /// 命令行参数解析结果。 /// +/// 如果解析失败,此处包含错误类型;否则为 。 /// 如果解析失败,此处包含错误消息;否则为 。 -public readonly record struct CommandLineParsingResult(string? ErrorMessage) +public readonly record struct CommandLineParsingResult(CommandLineParsingError ErrorType, string? ErrorMessage) { /// /// 获取一个值,指示此解析是否成功。 /// public bool IsSuccess => ErrorMessage is null; + /// + /// 将另一个解析结果与当前实例合并。合并后,如果全部成功,则结果为成功;如果有任何一个失败,则结果为失败,并包含第一个失败的错误信息。 + /// + /// + /// 两个都失败,则会使用此实例的错误信息。 + /// + public CommandLineParsingResult Combine(CommandLineParsingResult other) + { + return (IsSuccess, other.IsSuccess) switch + { + (true, true) => Success, + (false, true) => this, + (true, false) => other, + (false, false) => other, + }; + } + + /// + /// 如果解析结果表示失败,则抛出一个异常,包含错误信息。 + /// + public void ThrowIfError() + { + if (IsSuccess) + { + return; + } + + throw ErrorType switch + { + CommandLineParsingError.OptionalArgumentNotFound => new CommandLineParseException(ErrorMessage!), + CommandLineParsingError.OptionalArgumentParseError => new CommandLineParseException(ErrorMessage!), + CommandLineParsingError.PositionalArgumentNotFound => new CommandLineParseException(ErrorMessage!), + CommandLineParsingError.BooleanValueParseError => new CommandLineParseValueException(ErrorMessage!), + CommandLineParsingError.DictionaryValueParseError => new CommandLineParseValueException(ErrorMessage!), + CommandLineParsingError.None => throw new CommandLineException("解析过程中没有发生任何错误。"), + _ => throw new CommandLineException("未知的命令行解析错误类型。"), + }; + } + + /// + /// 隐式转换运算符,允许将 直接转换为布尔值,表示解析是否成功。 + /// + /// 要转换的解析结果。 + /// 如果解析成功,返回 ;否则返回 + public static implicit operator bool(CommandLineParsingResult result) => result.IsSuccess; + /// /// 获取一个表示成功的解析结果。 /// - public static CommandLineParsingResult Success => new(null); + public static CommandLineParsingResult Success => new(CommandLineParsingError.None, null); /// /// 创建一个表示选项未找到的解析结果。 @@ -24,11 +73,10 @@ public readonly record struct CommandLineParsingResult(string? ErrorMessage) /// 正在解析此参数的命令对象的名称。 /// 确定没有找到的选项名称。 /// 表示选项未找到的解析结果。 - public static CommandLineParsingResult OptionNotFound(CommandLine commandLine, int index, - string commandObjectName, ReadOnlySpan optionName) + public static CommandLineParsingResult OptionalArgumentNotFound(CommandLine commandLine, int index, string commandObjectName, ReadOnlySpan optionName) { var message = $"命令行对象 {commandObjectName} 不包含选项 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; - return new CommandLineParsingResult(message); + return new CommandLineParsingResult(CommandLineParsingError.OptionalArgumentNotFound, message); } /// @@ -37,10 +85,10 @@ public static CommandLineParsingResult OptionNotFound(CommandLine commandLine, i /// 整个命令行参数列表。 /// 当前正在解析的参数索引。 /// 表示选项未找到的解析结果。 - public static CommandLineParsingResult OptionParseError(CommandLine commandLine, int index) + public static CommandLineParsingResult OptionalArgumentParseError(CommandLine commandLine, int index) { var message = $"参数 {commandLine.CommandLineArguments[index]} 未能解析出选项名。参数列表:{commandLine},索引 {index}。"; - return new CommandLineParsingResult(message); + return new CommandLineParsingResult(CommandLineParsingError.OptionalArgumentParseError, message); } /// @@ -53,7 +101,68 @@ public static CommandLineParsingResult OptionParseError(CommandLine commandLine, /// 表示位置参数未找到的解析结果。 public static CommandLineParsingResult PositionalArgumentNotFound(CommandLine commandLine, int index, string commandObjectName, int positionalArgumentIndex) { - var message = $"命令行对象 {commandObjectName} 位置参数范围不包含索引 {positionalArgumentIndex}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; - return new CommandLineParsingResult(message); + var message = + $"命令行对象 {commandObjectName} 位置参数范围不包含索引 {positionalArgumentIndex}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; + return new CommandLineParsingResult(CommandLineParsingError.PositionalArgumentNotFound, message); + } + + /// + /// 创建一个表示无法将值解析为布尔值的解析结果。 + /// + /// 整个命令行参数列表。 + /// 要解析的值。 + /// 表示无法将值解析为布尔值的解析结果。 + public static CommandLineParsingResult BooleanValueParseError(CommandLine commandLine, ReadOnlySpan value) + { + var message = $"无法将 {value.ToString()} 解析为布尔值。参数列表:{commandLine}。"; + return new CommandLineParsingResult(CommandLineParsingError.BooleanValueParseError, message); } + + /// + /// 创建一个表示无法将值解析为键值对的解析结果。 + /// + /// 整个命令行参数列表。 + /// 要解析的值。 + /// 表示无法将值解析为键值对的解析结果。 + public static CommandLineParsingResult DictionaryValueParseError(CommandLine commandLine, ReadOnlySpan value) + { + var message = $"无法将 {value.ToString()} 解析为键值对。参数列表:{commandLine}。"; + return new CommandLineParsingResult(CommandLineParsingError.DictionaryValueParseError, message); + } +} + +/// +/// 命令行参数解析错误类型。 +/// +public enum CommandLineParsingError : byte +{ + /// + /// 没有错误。 + /// + None, + + /// + /// 没有任何选项能够匹配当前的命令行参数。 + /// + OptionalArgumentNotFound, + + /// + /// 当前的命令行参数无法解析出选项名。 + /// + OptionalArgumentParseError, + + /// + /// 没有任何位置参数的范围能够匹配当前的命令行参数。 + /// + PositionalArgumentNotFound, + + /// + /// 无法将值解析为布尔值。 + /// + BooleanValueParseError, + + /// + /// 无法将值解析为键值对。 + /// + DictionaryValueParseError, } From ac779196d37fbfe60c461522fec1ff0712c00d1d Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 13:36:02 +0800 Subject: [PATCH 028/193] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=A2=B3=E7=90=86?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 31 +++++++++++-------- .../DotNetCommandLineParserTests.cs | 8 ++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index fce455eb..7d762ca5 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -289,26 +289,27 @@ setPositionalArgumentProperties.Count is 0 private string GenerateInitProperty(PropertyGeneratingModel model) { - // 对于不同的属性类型,生成不同的代码。 - // init: 属性要求必须立即赋值 + // 对于不同的属性种类,如果命令行中没有赋值,则行为不同。 + + // required: 属性要求必须由命令行传入 + // init: 属性要求必须赋值(没有传入则使用该类型默认值) // nullable: 属性允许为 null // list: 属性是一个集合 // cli: 实际命令行参数是否传入 - // | init | nullable | list | cli | 行为 | - // | ---- | -------- | ---- | --- | ---------- | - // | 1 | 1 | _ | 0 | null | - // | 1 | 0 | 1 | 0 | 空集合 | - // | 1 | 0 | 0 | 0 | 抛异常 | - // | 0 | _ | _ | 0 | 保留初值 | - // | _ | _ | _ | 1 | 赋值 | + // | required | init | list | nullable | 行为 | 解释 | + // | -------- | ---- | ---- | -------- | ---------- | --------------------------------- | + // | 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | + // | 0 | 1 | 1 | _ | 空集合 | 集合永不为 null,没传就赋值空集合 | + // | 0 | 1 | 0 | 1 | null | 可空,没有传就赋值 null | + // | 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | + // | 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | var toTarget = model.Type.ToCommandValueNonAbstractName(); - var fallback = (model.IsNullable, model.Type.AsCommandValueKind() is CommandValueKind.List or CommandValueKind.Dictionary) switch + var isList = model.Type.AsCommandValueKind() is CommandValueKind.List or CommandValueKind.Dictionary; + var fallback = (model.IsRequired, model.IsInitOnly, isList, model.IsNullable) switch { - (true, _) => " ?? null", - (false, true) => "", - (false, false) => model switch + (true, _, _, _) => model switch { OptionalArgumentPropertyGeneratingModel option => $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", @@ -316,6 +317,10 @@ private string GenerateInitProperty(PropertyGeneratingModel model) $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", _ => "", }, + (_, true, true, _) => "", + (_, true, false, true) => " ?? null", + (_, true, false, false) => $" ?? default({model.Type.ToDisplayString()})", + _ => "/* 非 init 属性,在下面单独赋值 */", }; return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){fallback},"; } diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs index d6c65370..6b1c2e4f 100644 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs @@ -913,7 +913,7 @@ public void DotNetStyle_TwoCharShortOption_Parsed() // Act CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Tl) + .AddHandler(o => value = o.TerminalLogger) .Run(); // Assert @@ -929,7 +929,7 @@ public void DotNetStyle_SlashTwoCharOption_Parsed() // Act CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Tl) + .AddHandler(o => value = o.TerminalLogger) .Run(); // Assert @@ -1141,8 +1141,8 @@ internal record DotNet28_DotNetSpecificOptions internal record DotNet29_TwoCharOptions { - [Option("tl")] - public string Tl { get; init; } = string.Empty; + [Option("tl", "terminal-logger")] + public string TerminalLogger { get; init; } = string.Empty; } #endregion From 839b7d2a13e54c79054dd1d62fbedf790a81da5e Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 13:40:07 +0800 Subject: [PATCH 029/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=BC=80?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index aedd01b6..4343396d 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -197,18 +197,20 @@ public CommandNamingPolicy NamingPolicy public CommandOptionPrefix OptionPrefix { // [2] 表示是否使用短横线(-)作为选项前缀 - // [3] 表示长选项是否使用双短横线(--) + // [3] 表示是否使用斜杠(/)作为选项前缀 get => _booleans[2, 3]switch { - (true, true) => CommandOptionPrefix.DoubleDash, - (true, false) => CommandOptionPrefix.SingleDash, - (false, _) => CommandOptionPrefix.Slash, + (true, true) => CommandOptionPrefix.Any, + (true, false) => CommandOptionPrefix.DoubleDash, + (false, true) => CommandOptionPrefix.Slash, + (false, false) => CommandOptionPrefix.SingleDash, }; init => _booleans[2, 3] = value switch { - CommandOptionPrefix.DoubleDash => (true, true), - CommandOptionPrefix.SingleDash => (true, false), - CommandOptionPrefix.Slash => (false, false), + CommandOptionPrefix.Any => (true, true), + CommandOptionPrefix.DoubleDash => (true, false), + CommandOptionPrefix.Slash => (false, true), + CommandOptionPrefix.SingleDash => (false, false), _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), }; } From 638cca2f20d1d4a5f0f0732e0c8c5d8d400e60b3 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 13:51:44 +0800 Subject: [PATCH 030/193] =?UTF-8?q?=E5=8D=95=E7=8B=AC=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E4=B8=AD=E7=9A=84=E7=A9=BA=E6=A0=BC=E5=88=86?= =?UTF-8?q?=E9=9A=94=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 50 +++++++++++++++---- .../Utils/Parsers/CommandLineParser.cs | 14 ++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 4343396d..2ccae136 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -18,10 +18,12 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.Both, OptionPrefix = CommandOptionPrefix.Any, - OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), - CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + OptionValueSeparators = CommandSeparatorChars.Create(':', '='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; @@ -36,10 +38,12 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = true, SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, - OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), - CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + OptionValueSeparators = CommandSeparatorChars.Create(':', '='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; @@ -54,10 +58,12 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = true, SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = true, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = false, NamingPolicy = CommandNamingPolicy.KebabCase, OptionPrefix = CommandOptionPrefix.DoubleDash, - OptionValueSeparators = CommandSeparatorChars.Create('=', ' '), - CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + OptionValueSeparators = CommandSeparatorChars.Create('='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; @@ -72,9 +78,11 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.PascalCase, OptionPrefix = CommandOptionPrefix.SingleDash, - OptionValueSeparators = CommandSeparatorChars.Create(' '), + OptionValueSeparators = CommandSeparatorChars.Create(), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; @@ -90,10 +98,12 @@ public readonly record struct CommandLineParsingOptions SupportsShortOptionCombination = false, SupportsMultiCharShortOption = true, SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.PascalCase, OptionPrefix = CommandOptionPrefix.Slash, - OptionValueSeparators = CommandSeparatorChars.Create(':', '=', ' '), - CollectionValueSeparators = CommandSeparatorChars.Create(',', ';', ' '), + OptionValueSeparators = CommandSeparatorChars.Create(':', '='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; @@ -281,6 +291,28 @@ public bool SupportsShortOptionValueWithoutSeparator init => _booleans[9] = value; } + /// + /// 是否支持使用空格分隔选项名和选项值。
+ /// 例如 --option value 等同于 --option=value。
+ /// 如果为 ,则 --option value 会被视为 --option 选项,value 会被视为下一个位置参数或选项。 + ///
+ public bool SupportsSpaceSeparatedOptionValue + { + get => _booleans[10]; + init => _booleans[10] = value; + } + + /// + /// 当选项值为集合类型时,是否支持使用空格分隔多个选项值。
+ /// 例如 --option value1 value2 等同于 --option value1,value2。
+ /// 如果为 ,则 --option value1 value2 会被视为 --option 的值为 "value1",而 "value2" 会被视为下一个位置参数或选项。 + ///
+ public bool SupportsSpaceSeparatedCollectionValues + { + get => _booleans[11]; + init => _booleans[11] = value; + } + /// /// 允许用户使用哪些分隔符来分隔选项名和选项值。
/// 如 ':', '=', ' ' 分别对应: --option:value, --option=value, --option value。 diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index dcf0f2f0..261de6b3 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -33,6 +33,8 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int SupportsShortOptionCombination = Style.SupportsShortOptionCombination; SupportsMultiCharShortOption = Style.SupportsMultiCharShortOption; SupportsShortOptionValueWithoutSeparator = Style.SupportsShortOptionValueWithoutSeparator; + SupportsSpaceSeparatedOptionValue = Style.SupportsSpaceSeparatedOptionValue; + SupportsSpaceSeparatedCollectionValues = Style.SupportsSpaceSeparatedCollectionValues; } /// @@ -58,6 +60,12 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int /// internal bool SupportsShortOptionValueWithoutSeparator { get; } + /// + internal bool SupportsSpaceSeparatedOptionValue { get; } + + /// + internal bool SupportsSpaceSeparatedCollectionValues { get; } + /// /// 要求源生成器匹配长名称,返回此长选项的值类型。 /// @@ -245,6 +253,12 @@ public CommandLineParsingResult Parse() return result; } + /// + /// 配合源生成器生成的匹配结果,将选项值赋值给指定索引处的属性。 + /// + /// 源生成器生成的匹配结果。 + /// 选项值。 + /// 命令行参数解析结果。 private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadOnlySpan value) { var result = CommandLineParsingResult.Success; From 406f7cac869be8cd1f5f0fd5623928b432c95ad6 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 14:20:33 +0800 Subject: [PATCH 031/193] =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E6=8A=9B=E5=87=BA=E6=AD=A3=E7=A1=AE=E7=9A=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 2 +- .../Compiler/PropertyAssignments.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 7d762ca5..8ca183da 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -385,7 +385,7 @@ private string GenerateEnumDeclarationCode(ITypeSymbol enumType) { {{string.Join("\n ", enumNames.Select(x => $" \"{x.ToLowerInvariant()}\" => {enumType.ToUsingString()}.{x},"))}} _ when IgnoreExceptions => null, - _ => throw new global::System.ArgumentOutOfRangeException(nameof(value), value.ToString(), $"Cannot convert '{value.ToString()}' to enum type '{{enumType.ToDisplayString()}}'."), + _ => throw new global::DotNetCampus.Cli.Exceptions.CommandLineParseValueException($"Cannot convert '{value.ToString()}' to enum type '{{enumType.ToDisplayString()}}'."), }; return this with { Value = newValue }; } diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index 90dc42e3..5288cf83 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -3,6 +3,7 @@ #endif using System.Collections.ObjectModel; using System.Diagnostics; +using DotNetCampus.Cli.Exceptions; namespace DotNetCampus.Cli.Compiler; @@ -76,7 +77,7 @@ public NumberArgument Assign(ReadOnlySpan value) } if (!IgnoreExceptions) { - throw new FormatException($"无法将 \"{value.ToString()}\" 转换为数值。"); + throw new CommandLineParseValueException($"无法将 \"{value.ToString()}\" 转换为数值。"); } return this; } @@ -181,7 +182,7 @@ public StringArgument Assign(ReadOnlySpan value) null => null, { Length: 1 } => Value[0], _ when IgnoreExceptions => null, - _ => throw new FormatException($"无法将 \"{Value}\" 转换为字符,因为它的长度不为 1。"), + _ => throw new CommandLineParseValueException($"无法将 \"{Value}\" 转换为字符,因为它的长度不为 1。"), }; /// @@ -311,7 +312,7 @@ public StringDictionaryArgument Append(ReadOnlySpan key, ReadOnlySpan 1) { - throw new InvalidOperationException("字典包含多个元素,无法转换为 KeyValuePair。"); + throw new CommandLineParseValueException("字典包含多个元素,无法转换为 KeyValuePair。"); } using var enumerator = Value.GetEnumerator(); @@ -363,7 +364,7 @@ public RuntimeEnumArgument Assign(ReadOnlySpan value) } if (!IgnoreExceptions) { - throw new FormatException($"无法将 \"{value.ToString()}\" 转换为 {typeof(T).FullName} 枚举。"); + throw new CommandLineParseValueException($"无法将 \"{value.ToString()}\" 转换为 {typeof(T).FullName} 枚举。"); } return this; } From 17b2b1b8fcc4060a941dcc23a2f2d84dcf3327f4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 14:33:00 +0800 Subject: [PATCH 032/193] =?UTF-8?q?DotNet=20=E9=A3=8E=E6=A0=BC=E4=B8=AD?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=80=82=E7=94=A8=E7=9A=84=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=8C=E5=BF=BD=E7=95=A5=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DotNetCommandLineParserTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs index 6b1c2e4f..0bbd9b81 100644 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs @@ -21,6 +21,7 @@ public class DotNetCommandLineParserTests #region 1. 选项识别与解析 + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.1. 短选项冒号形式 (-option:value),字符串类型,可正常赋值。")] public void ShortOption_WithColon_StringType_ValueAssigned() { @@ -53,6 +54,7 @@ public void LongOption_WithColon_StringType_ValueAssigned() Assert.AreEqual("test", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.3. 斜杠前缀形式 (/option:value),字符串类型,可正常赋值。")] public void SlashPrefix_WithColon_StringType_ValueAssigned() { @@ -69,6 +71,7 @@ public void SlashPrefix_WithColon_StringType_ValueAssigned() Assert.AreEqual("test", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.4. 多个选项混合使用,全部正确解析。")] public void MixedOptions_MultipleParsed_AllAssigned() { @@ -94,6 +97,7 @@ public void MixedOptions_MultipleParsed_AllAssigned() Assert.IsTrue(flag); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.5. PascalCase命名风格选项,可正常解析。")] public void PascalCaseOption_Parsed_ValueAssigned() { @@ -142,6 +146,7 @@ public void KebabCaseOption_Parsed_ValueAssigned() Assert.AreEqual("value", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.8. 不同前缀的PascalCase风格选项,可正常解析。")] public void MixedPrefixWithPascalCase_Parsed_ValueAssigned() { @@ -167,6 +172,7 @@ public void MixedPrefixWithPascalCase_Parsed_ValueAssigned() Assert.AreEqual("value3", option3); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("1.9. 单字符短选项,不同前缀,可正常解析。")] public void SingleCharOptions_DifferentPrefixes_Parsed() { @@ -327,6 +333,7 @@ public void StringArrayOption_CommaSeparated_ValueAssigned() CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.4.3. 字符串数组,包含带引号的值,赋值成功。")] public void StringArrayOption_QuotedValues_ValueAssigned() { @@ -349,6 +356,7 @@ public void StringArrayOption_QuotedValues_ValueAssigned() CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.4.4. 字符串数组,使用分号分隔的带引号值,赋值成功。")] public void StringArrayOption_SemicolonSeparatedQuoted_ValueAssigned() { @@ -584,6 +592,7 @@ public void TypeMismatch_ThrowsException() }); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.4. 大小写不敏感,识别正确。")] public void CaseInsensitive_CorrectOptionParsed() { @@ -708,6 +717,7 @@ public void MixedPositionalAndOptions_AllParsedCorrectly() Assert.AreEqual("value2", value2); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("5.4. 指定索引位置参数,识别正确。")] public void IndexedPositionalValues_CorrectAssignment() { @@ -872,6 +882,7 @@ public void DotNetStyle_DoubleDashPascalCase_Parsed() Assert.AreEqual("value", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("8.2. DotNet风格,单破折号+PascalCase,可正常解析。")] public void DotNetStyle_SingleDashPascalCase_Parsed() { @@ -888,6 +899,7 @@ public void DotNetStyle_SingleDashPascalCase_Parsed() Assert.AreEqual("value", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("8.3. DotNet风格,斜杠+PascalCase,可正常解析。")] public void DotNetStyle_SlashPascalCase_Parsed() { @@ -920,6 +932,7 @@ public void DotNetStyle_TwoCharShortOption_Parsed() Assert.AreEqual("off", value); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("8.5. DotNet风格,斜杠前缀两字符短选项,可正常解析。")] public void DotNetStyle_SlashTwoCharOption_Parsed() { From bb3ea9913afaf96a9b700ef596c1ac8986c32b5a Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 14:46:54 +0800 Subject: [PATCH 033/193] =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E8=B7=9F=E4=B8=80=E4=B8=AA=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 261de6b3..12ae391a 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -481,7 +481,7 @@ private bool ParseOptionAndPositionalArgumentRegion() Cat.PositionalArgument or Cat.LongOptionWithValue or Cat.ShortOptionWithValue or Cat.OptionWithValue => ParseOptionOrPositionalArgument(), // 多个短选项,后面不允许带值。 Cat.MultiShortOptions => ParseOptionOrPositionalArgument(), - // 上一个是选项: + // 上一个是选项。 Cat.LongOption or Cat.ShortOption or Cat.Option => (_lastOptionType switch { // 如果是布尔选项,则后面只能跟布尔值,否则只能是新的选项或位置参数。 @@ -491,6 +491,15 @@ private bool ParseOptionAndPositionalArgumentRegion() // 如果是普通选项,则后面只能是选项值。 _ => ParseOptionValue(_argument.AsSpan()), }), + // 上一个是选项的值。 + Cat.OptionValue => (_lastOptionType switch + { + // 只有集合才可以继续跟值(且必须允许),其他都要解析为新的选项或位置参数。 + OptionValueType.List or OptionValueType.Dictionary when _parser.SupportsSpaceSeparatedCollectionValues => + ParseCollectionOptionValueOrNewOptionOrPositionalArgument(), + // 解析为新的选项或位置参数。 + _ => ParseOptionOrPositionalArgument(), + }), _ => throw new InvalidOperationException($"解析上一个参数时已进入错误的状态:{_lastType}。"), }; } From bcb381156f87c602b8ca445cb39fa428048a36c1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 15:14:31 +0800 Subject: [PATCH 034/193] =?UTF-8?q?=E6=A2=B3=E7=90=86=E5=A4=9A=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E7=BB=84=E5=90=88=E7=9A=84=E7=9F=AD=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E7=9A=84=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 5 +- .../Utils/Parsers/CommandArgumentType.cs | 13 ++- .../Utils/Parsers/CommandLineParser.cs | 96 +++++++------------ .../GnuCommandLineParserTests.cs | 6 ++ .../PosixCommandLineParserTests.cs | 1 + 5 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 2ccae136..2d25380e 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -75,13 +75,14 @@ public readonly record struct CommandLineParsingOptions CaseSensitive = true, SupportsLongOption = false, SupportsShortOption = true, - SupportsShortOptionCombination = false, + SupportsShortOptionCombination = true, SupportsMultiCharShortOption = false, SupportsShortOptionValueWithoutSeparator = false, SupportsSpaceSeparatedOptionValue = true, SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.PascalCase, - OptionPrefix = CommandOptionPrefix.SingleDash, + // Posix 不支持长选项,使用 DoubleDash 的含义是 '-' 一定表示短选项。 + OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create(), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs index 2e7cb792..34b0f8e0 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs @@ -58,13 +58,16 @@ internal enum CommandArgumentType /// /// 多个短选项。-abc /// + /// + /// 存在以下三种情况: + /// + /// -abc 表示 -a -b -c 三个布尔短选项。 + /// -abc 表示 -a 选项的值为 bc。-abc 表示一个名为 abc 的多字符短选项。 + /// + /// MultiShortOptions, - /// - /// 不确定是多个短选项,还是一个无分隔符的带值短选项。-a1.txt - /// - MultiShortOptionsOrShortOptionConcatWithValue, - /// /// 选项值。value /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 12ae391a..41705a7b 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -192,48 +192,51 @@ public CommandLineParsingResult Parse() } case Cat.MultiShortOptions: { - // 如果支持多字符短选项,则优先作为多字符短选项处理。 + var name = optionName.Name; if (SupportsMultiCharShortOption) { - var m = optionName.Name; - var optionMatch = MatchShortOption(m, _caseSensitive); + // 如果支持多字符短选项,则优先作为多字符短选项处理。 + var optionMatch = MatchShortOption(name, _caseSensitive); if (optionMatch.ValueType is not OptionValueType.NotExist) { // 是一个多字符短选项。 result = AssignOptionValue(optionMatch, []).Combine(result); - break; } + break; } - // 随后,尝试逐个处理多个短选项。 - for (var i = 0; i < optionName.Name.Length; i++) + var allCombined = SupportsShortOptionCombination; + if (SupportsShortOptionCombination) { - var n = optionName.Name[i..(i + 1)]; - var optionMatch = MatchShortOption(n, _caseSensitive); - if (optionMatch.ValueType is OptionValueType.NotExist) + // 如果不支持,则尝试逐个处理多个短选项。 + for (var i = 0; i < optionName.Name.Length; i++) { - // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, n); + var n = optionName.Name[i..(i + 1)]; + var optionMatch = MatchShortOption(n, _caseSensitive); + if (optionMatch.ValueType is OptionValueType.NotExist) + { + // 如果选项不存在,则报告错误。 + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, n); + } + if (optionMatch.ValueType is OptionValueType.Boolean) + { + AssignOptionValue(optionMatch, []); + } + else + { + // 只有布尔选项才允许组合。 + allCombined = false; + break; + } } - result = AssignOptionValue(optionMatch, []).Combine(result); } - break; - } - case Cat.MultiShortOptionsOrShortOptionConcatWithValue: - { - var name = optionName.Name; - // 如果支持多字符短选项,则优先作为多字符短选项处理。 - if (SupportsMultiCharShortOption) + if (allCombined) { - var optionMatch = MatchShortOption(name, _caseSensitive); - if (optionMatch.ValueType is not OptionValueType.NotExist) - { - // 是一个多字符短选项。 - result = AssignOptionValue(optionMatch, []).Combine(result); - break; - } + // 短选项组合已处理完所有字符。 + break; } - // 随后,尝试解析为单个字符无分隔符带值的短选项。 + if (SupportsShortOptionValueWithoutSeparator) { + // 随后,尝试解析为单个字符无分隔符带值的短选项。 var o = name[..1]; var v = name[1..]; var optionMatch = MatchShortOption(o, _caseSensitive); @@ -243,8 +246,10 @@ public CommandLineParsingResult Parse() return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, o); } result = AssignOptionValue(optionMatch, v).Combine(result); + break; } - break; + // 能进到这里,说明短选项的上述三种情况都不满足,应该报告错误。 + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, name); } // 其他状态要么已经处理过了,要不还未处理,要么不需要处理,所以不需要做任何事情。 } @@ -638,37 +643,10 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) return true; } - // 对于不带值的短选项,存在以下三种情况: - // 1. -abc 表示 -a -b -c 三个布尔短选项。 - // 2. -abc 表示 -a 选项的值为 bc。 - // 3. -abc 表示一个名为 abc 的多字符短选项。 - // 目前不存在任何一种命令行风格同时支持上述三种情况,所以我们可以消除一些不确定性。 - var supportsCombination = _parser.SupportsShortOptionCombination; - var supportsNoSeparator = _parser.SupportsShortOptionValueWithoutSeparator; - switch (supportsCombination, supportsNoSeparator) - { - // 支持短选项组合,也支持无分隔符带值的短选项。(上述 1 和 2,从实际考虑消除了 3) - case (true, true): - Type = Cat.MultiShortOptionsOrShortOptionConcatWithValue; - Option = new OptionName(false, argument); - return true; - case (true, false): - // 支持短选项组合,不支持无分隔符带值的短选项。(上述 1 和 3) - Type = Cat.MultiShortOptions; - Option = new OptionName(false, argument); - return true; - case (false, true): - // 不支持短选项组合,但支持无分隔符带值的短选项。(上述 2,从实际考虑消除了 3) - Type = Cat.ShortOptionWithValue; - Option = new OptionName(false, argument[..1]); - Value = argument[1..]; - return true; - case (false, false): - // 既不支持短选项组合,也不支持无分隔符带值的短选项。(上述 3) - Type = Cat.ShortOption; - Option = new OptionName(false, argument); - return true; - } + // 直接返回,延迟处理。 + Type = Cat.MultiShortOptions; + Option = new OptionName(false, argument); + return true; } private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) diff --git a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs index d6bea3e4..9b861f8c 100644 --- a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs @@ -271,6 +271,7 @@ public void CommaSeparatedList_ValueAssigned() CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.8. 带引号的列表参数,赋值成功。")] public void QuotedArrayOption_ValueAssigned() { @@ -291,6 +292,7 @@ public void QuotedArrayOption_ValueAssigned() CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.9. 等号方式带引号的列表参数,赋值成功。")] public void QuotedArrayWithEquals_ValueAssigned() { @@ -420,6 +422,7 @@ public void SingleOptionCaseSensitive_GlobalInsensitive_CorrectlyParsed() Assert.AreEqual("value2", insensitiveValue); // 大小写不敏感,匹配第二个 case-option } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.7. 单个选项设置大小写不敏感,全局设置为敏感,识别正确。")] public void OptionCaseInsensitive_OverridesGlobalSensitive() { @@ -442,6 +445,7 @@ public void OptionCaseInsensitive_OverridesGlobalSensitive() Assert.AreEqual("value2", option2Value); // 选项明确指定为大小写不敏感,所以匹配成功 } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.8. 全局大小写敏感时,未指定大小写设置的选项不匹配。")] public void GlobalCaseSensitive_DefaultOption_NotMatched() { @@ -461,6 +465,7 @@ public void GlobalCaseSensitive_DefaultOption_NotMatched() Assert.IsNull(globalSensitiveValue); // 全局大小写敏感,--global-sensitive 不匹配 --GLOBAL-SENSITIVE } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.9. 选项设置大小写敏感时,大小写不匹配无效。")] public void OptionCaseSensitive_CaseMismatch_NotMatched() { @@ -654,6 +659,7 @@ public void MixedPositionalAndOptions_AllParsedCorrectly() Assert.AreEqual("value2", value2); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("5.4. 指定索引位置参数,识别正确。")] public void IndexedPositionalValues_CorrectAssignment() { diff --git a/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs index a2c04c2d..73315530 100644 --- a/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs @@ -336,6 +336,7 @@ public void MultipleOptions_FormList() CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("7.2. 带引号的列表参数")] public void QuotedArrayElements() { From c8c3eccdb2a34d31761f107871f08780605d61ff Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 15:47:16 +0800 Subject: [PATCH 035/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20PowerShell=20?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E5=89=8D=E7=BC=80=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 8 +-- .../CommandLineParsingOptions.cs | 66 ++++++++++--------- .../Utils/Parsers/CommandArgumentType.cs | 2 +- .../Utils/Parsers/CommandLineParser.cs | 16 +++++ .../PowerShellCommandLineParserTests.cs | 1 + 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 8ca183da..3f4a89e0 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -111,18 +111,12 @@ private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 if (namingPolicy.SupportsPascalCase()) { - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames().Except(x.GetOrdinalLongNames()).ToList())))}} + {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames())))}} } """; static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { - if (names.Count == 0) - { - return $""" - // 属性 {model.PropertyName} 在此命名法下的所有名称均已在前面匹配过,无需重复匹配。 - """; - } var comparison = model.CaseSensitive switch { true => "global::System.StringComparison.Ordinal", diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 2d25380e..b042841c 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -102,7 +102,7 @@ public readonly record struct CommandLineParsingOptions SupportsSpaceSeparatedOptionValue = true, SupportsSpaceSeparatedCollectionValues = true, NamingPolicy = CommandNamingPolicy.PascalCase, - OptionPrefix = CommandOptionPrefix.Slash, + OptionPrefix = CommandOptionPrefix.SlashOrDash, OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -207,22 +207,22 @@ public CommandNamingPolicy NamingPolicy /// public CommandOptionPrefix OptionPrefix { - // [2] 表示是否使用短横线(-)作为选项前缀 - // [3] 表示是否使用斜杠(/)作为选项前缀 - get => _booleans[2, 3]switch + get => _booleans[2, 3, 4]switch { - (true, true) => CommandOptionPrefix.Any, - (true, false) => CommandOptionPrefix.DoubleDash, - (false, true) => CommandOptionPrefix.Slash, - (false, false) => CommandOptionPrefix.SingleDash, + (false, false, false) => CommandOptionPrefix.DoubleDash, + (false, false, true) => CommandOptionPrefix.SingleDash, + (false, true, false) => CommandOptionPrefix.Slash, + (false, true, true) => CommandOptionPrefix.SlashOrDash, + (true, _, _) => CommandOptionPrefix.Any, }; - init => _booleans[2, 3] = value switch + init => _booleans[2, 3, 4] = value switch { - CommandOptionPrefix.Any => (true, true), - CommandOptionPrefix.DoubleDash => (true, false), - CommandOptionPrefix.Slash => (false, true), - CommandOptionPrefix.SingleDash => (false, false), - _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), + CommandOptionPrefix.DoubleDash => (false, false, false), + CommandOptionPrefix.SingleDash => (false, false, true), + CommandOptionPrefix.Slash => (false, true, false), + CommandOptionPrefix.SlashOrDash => (false, true, true), + CommandOptionPrefix.Any => (true, false, false), + _ => (true, true, true), }; } @@ -231,8 +231,8 @@ public CommandOptionPrefix OptionPrefix /// public bool CaseSensitive { - get => _booleans[4]; - init => _booleans[4] = value; + get => _booleans[5]; + init => _booleans[5] = value; } /// @@ -240,8 +240,8 @@ public bool CaseSensitive /// public bool SupportsLongOption { - get => _booleans[5]; - init => _booleans[5] = value; + get => _booleans[6]; + init => _booleans[6] = value; } /// @@ -249,8 +249,8 @@ public bool SupportsLongOption /// public bool SupportsShortOption { - get => _booleans[6]; - init => _booleans[6] = value; + get => _booleans[7]; + init => _booleans[7] = value; } /// @@ -263,8 +263,8 @@ public bool SupportsShortOption /// public bool SupportsShortOptionCombination { - get => _booleans[7]; - init => _booleans[7] = value; + get => _booleans[8]; + init => _booleans[8] = value; } /// @@ -276,8 +276,8 @@ public bool SupportsShortOptionCombination /// public bool SupportsMultiCharShortOption { - get => _booleans[8]; - init => _booleans[8] = value; + get => _booleans[9]; + init => _booleans[9] = value; } /// @@ -288,8 +288,8 @@ public bool SupportsMultiCharShortOption /// public bool SupportsShortOptionValueWithoutSeparator { - get => _booleans[9]; - init => _booleans[9] = value; + get => _booleans[10]; + init => _booleans[10] = value; } /// @@ -299,8 +299,8 @@ public bool SupportsShortOptionValueWithoutSeparator /// public bool SupportsSpaceSeparatedOptionValue { - get => _booleans[10]; - init => _booleans[10] = value; + get => _booleans[11]; + init => _booleans[11] = value; } /// @@ -310,8 +310,8 @@ public bool SupportsSpaceSeparatedOptionValue /// public bool SupportsSpaceSeparatedCollectionValues { - get => _booleans[11]; - init => _booleans[11] = value; + get => _booleans[12]; + init => _booleans[12] = value; } /// @@ -386,6 +386,12 @@ public enum CommandOptionPrefix : byte /// Slash, + /// + /// 使用斜杠(/)或单个短横线(-)作为长选项和短选项前缀。
+ /// 注意:如果启用此选项,将不支持短选项组合和短选项直接跟值;仍支持多字符短选项,但解析会造成轻微的性能下降(因为会两次尝试匹配选项名)。 + ///
+ SlashOrDash, + /// /// 允许使用任意一种前缀风格(-、--、/)。
/// 注意:如果启用此选项,将不支持短选项组合和短选项直接跟值;仍支持多字符短选项,但解析会造成轻微的性能下降(因为会两次尝试匹配选项名)。 diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs index 34b0f8e0..b484c9ad 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandArgumentType.cs @@ -62,7 +62,7 @@ internal enum CommandArgumentType /// 存在以下三种情况: /// /// -abc 表示 -a -b -c 三个布尔短选项。 - /// -abc 表示 -a 选项的值为 bc。-abc 表示 -a 选项的值为 bc。 /// -abc 表示一个名为 abc 的多字符短选项。 /// /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 41705a7b..7f3d50e5 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -571,6 +571,11 @@ private bool ParseOptionOrPositionalArgument() '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), _ => ParsePositionalArgument(argument), }, + CommandOptionPrefix.SlashOrDash => argument[0] switch + { + '-' or '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParsePositionalArgument(argument), + }, CommandOptionPrefix.Any => (argument[0], argument[1]) switch { ('-', '-') => ParseLongOptionOrLongOptionWithValue(argument[2..]), @@ -729,6 +734,17 @@ private bool ParseCollectionOptionValueOrNewOptionOrPositionalArgument() '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), _ => ParseOptionValue(argument), }, + CommandOptionPrefix.SlashOrDash => argument[0] switch + { + '-' or '/' => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParseOptionValue(argument), + }, + CommandOptionPrefix.Any => (argument[0], argument[1]) switch + { + ('-', '-') => ParseLongOptionOrLongOptionWithValue(argument[2..]), + ('-', _) or ('/', _) => ParseLongShortOptionOrLongShortOptionWithValue(argument[1..]), + _ => ParseOptionValue(argument), + }, _ => throw new ArgumentOutOfRangeException(), }; } diff --git a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs index 6c87309d..3e8d3ed1 100644 --- a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs @@ -342,6 +342,7 @@ public void SemicolonSeparatedArrayParameter() CollectionAssert.AreEqual(new[] { "chrome", "firefox", "edge" }, processes); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("5.6. 逗号分隔的带引号数组元素。")] public void CommaSeparatedQuotedArrayElements() { From ac6ee9cb2d6b1912c9fce075cdeaed45a20530bb Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 16:00:11 +0800 Subject: [PATCH 036/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=A9=E4=BD=99?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=82=B8=E6=8E=89=E7=9A=84=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandRunner.cs | 6 +++++- .../Utils/Handlers/TaskCommandHandler.cs | 3 +-- .../FlexibleCommandLineParserTests.cs | 7 +++++++ .../NamingConventionTests.cs | 6 +++++- tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs | 2 ++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index e6404b98..2af0413e 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -63,7 +63,11 @@ public Task RunAsync() { if (header.StartsWith(command, stringComparison)) { - return (header, factory); + // 前缀已匹配成功,接下来判断这是否是命令单词边界。 + if (header.Length == command.Length || char.IsWhiteSpace(header[command.Length])) + { + return (command, factory); + } } } } diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 9ac361d6..84ba9e84 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -55,8 +55,7 @@ public Task RunAsync() { throw new InvalidOperationException($"No options of type {typeof(T)} were created."); } - handler(_options); - return Task.FromResult(0); + return Task.FromResult(handler(_options)); } } diff --git a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs index 5ea5b255..1fe04d21 100644 --- a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs @@ -559,6 +559,7 @@ public void MixedNamingStyles_AllAssigned() #region 9. 边界情况和错误处理 + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("9.1. 未知选项,抛出异常")] public void UnknownOption_ThrowsException() { @@ -574,6 +575,7 @@ public void UnknownOption_ThrowsException() }); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("9.2. 选项名称拼写错误时,抛出异常并提示近似选项")] public void MisspelledOption_ThrowsExceptionWithHint() { @@ -715,6 +717,7 @@ public void MixedSeparatorList_ParsedCorrectly() CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("11.5. 支持带引号的列表参数")] public void QuotedListElements_ParsedCorrectly() { @@ -737,6 +740,7 @@ public void QuotedListElements_ParsedCorrectly() CollectionAssert.AreEqual(new[] { "file with spaces.txt", "another file.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("11.6. 带引号的列表参数,多次指定")] public void QuotedListElements_MultipleOptions() { @@ -759,6 +763,7 @@ public void QuotedListElements_MultipleOptions() CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("11.7. 带引号的列表参数,通过分隔符")] public void QuotedListElements_WithSeparators() { @@ -781,6 +786,7 @@ public void QuotedListElements_WithSeparators() CollectionAssert.AreEqual(new[] { "John Doe", "Jane Smith", "Anonymous" }, names.ToArray()); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("11.9. 单选项后接带引号且引号内有逗号或分号的多个值")] public void SingleOption_QuotedMultipleValuesWithColonOrSemicolon() { @@ -808,6 +814,7 @@ public void SingleOption_QuotedMultipleValuesWithColonOrSemicolon() CollectionAssert.AreEqual(new[] { "tag1;with;semicolon", "tag2;with;semicolon" }, tags.ToArray()); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("11.10. 单选项后接带引号且引号内有逗号或分号的多个值,其中部分引号和分隔符含空字符串")] public void SingleOption_QuotedMultipleValuesWithColonOrSemicolonAndEmpty() { diff --git a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs b/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs index 3c50ee9b..2e986e48 100644 --- a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs @@ -20,7 +20,7 @@ public class NamingConventionTests private CommandLineParsingOptions CaseSensitive { get; } = new CommandLineParsingOptions { - Style = CommandLineParsingOptions.Flexible.Style with { CaseSensitive = false }, + Style = CommandLineParsingOptions.Flexible.Style with { CaseSensitive = true }, }; #region 1. CommandAttribute 命名规则测试 @@ -267,6 +267,7 @@ public void Option_Aliases() Assert.AreEqual("lib", capturedOutput2); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.6. ExactSpelling 精确拼写")] public void Option_ExactSpelling() { @@ -297,6 +298,7 @@ public void Option_ExactSpelling() }); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("2.7. 选项大小写敏感性")] public void Option_CaseSensitive() { @@ -502,6 +504,7 @@ public void Mixed_MultipleNamingStyles() #region 5. 边界情况和错误处理测试 + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("5.1. 无效的短选项名(非字母字符)")] public void Error_InvalidShortOptionName() { @@ -520,6 +523,7 @@ public void Error_InvalidShortOptionName() }); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("5.2. 空字符串选项名")] public void Error_EmptyOptionName() { diff --git a/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs index 5e59c937..a94921c2 100644 --- a/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs @@ -342,6 +342,7 @@ public void LongestPathMatching_ComplexCase() Assert.IsTrue(clusterConfigSetContextHandlerCalled); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.6. 最长路径匹配 - 前缀匹配但非完整匹配")] public void LongestPathMatching_PrefixButNotComplete() { @@ -380,6 +381,7 @@ public void LongestPathMatching_CaseInsensitive() Assert.IsTrue(remoteAddHandlerCalled); } + [Ignore("规范行为后,此测试不再适用。")] [TestMethod("3.8. 最长路径匹配 - 单个字符差异")] public void LongestPathMatching_SingleCharacterDifference() { From 41fc0f2564f7f09e8b924cde90fc92c1588d2e2c Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 16:12:00 +0800 Subject: [PATCH 037/193] =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E5=88=B0=20CommandLine=20=E4=B8=AD=E7=9A=84=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandRunner.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index 2af0413e..47bf6dbe 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -11,6 +11,7 @@ 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 CommandObjectFactory? _defaultFactory; @@ -18,9 +19,13 @@ public class CommandRunner : ICommandRunnerBuilder, IAsyncCommandRunnerBuilder internal CommandRunner(CommandLine commandLine) { _commandLine = commandLine; - _factories = commandLine.DefaultCaseSensitive + var caseSensitive = commandLine.ParsingOptions.Style.CaseSensitive; + _factories = caseSensitive ? new SortedList(StringLengthDescendingComparer.CaseSensitive) : new SortedList(StringLengthDescendingComparer.CaseInsensitive); + _stringComparison = caseSensitive + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; _supportsOrdinal = commandLine.ParsingOptions.Style.NamingPolicy.SupportsOrdinal(); _supportsPascalCase = commandLine.ParsingOptions.Style.NamingPolicy.SupportsPascalCase(); } @@ -55,13 +60,10 @@ public Task RunAsync() { var maxLength = _factories.Keys[0].Length; var header = _commandLine.GetHeader(maxLength); - var stringComparison = _commandLine.DefaultCaseSensitive - ? StringComparison.Ordinal - : StringComparison.OrdinalIgnoreCase; foreach (var (command, factory) in _factories) { - if (header.StartsWith(command, stringComparison)) + if (header.StartsWith(command, _stringComparison)) { // 前缀已匹配成功,接下来判断这是否是命令单词边界。 if (header.Length == command.Length || char.IsWhiteSpace(header[command.Length])) From 04d4eef4961c89d78d27567d4dab33b5ef740115 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 17:00:39 +0800 Subject: [PATCH 038/193] =?UTF-8?q?=E6=94=AF=E6=8C=81=20URL=20=E7=9A=84?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandLine.cs | 25 +++- .../CommandLineParsingOptions.cs | 19 +++ .../Utils/Parsers/CommandLineParser.cs | 5 +- .../Utils/Parsers/CommandUrlParser.cs | 132 ++++++++++++++++++ .../UrlCommandLineParserTests.cs | 16 ++- 5 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index f426cbed..3bb36f91 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; +using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; @@ -11,10 +12,23 @@ namespace DotNetCampus.Cli; ///
public class CommandLine : ICoreCommandRunnerBuilder { + /// + /// 存储特殊处理过 URL 的命令行参数。 + /// + private readonly IReadOnlyList? _urlNormalizedArguments; + /// /// 获取此命令行解析类型所关联的命令行参数。 /// - public IReadOnlyList CommandLineArguments { get; } + /// + /// 如果命令行参数中传入的是 URL,则此参数不会保存原始的 URL,而是将 URL 转换为普通的命令行参数列表。 + /// + public IReadOnlyList CommandLineArguments => _urlNormalizedArguments ?? RawArguments; + + /// + /// 获取命令行传入的原始参数列表。 + /// + public IReadOnlyList RawArguments { get; } /// /// 获取解析此命令行时所使用的各种选项。 @@ -22,20 +36,21 @@ public class CommandLine : ICoreCommandRunnerBuilder public CommandLineParsingOptions ParsingOptions { get; } /// - /// 在特定的属性不指定时,默认应使用的大小写敏感性。 + /// 如果此命令行是从 Web 请求的 URL 中解析出来的,则此属性保存 URL 的 Scheme 部分。 /// - public bool DefaultCaseSensitive => ParsingOptions.Style.CaseSensitive; + internal string? MatchedUrlScheme { get; } private CommandLine() { - CommandLineArguments = []; + RawArguments = []; ParsingOptions = CommandLineParsingOptions.Flexible; } private CommandLine(IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions = null) { - CommandLineArguments = arguments; + RawArguments = arguments; ParsingOptions = parsingOptions ?? CommandLineParsingOptions.Flexible; + (MatchedUrlScheme, _urlNormalizedArguments) = CommandUrlParser.TryNormalizeUrlArguments(arguments, ParsingOptions); } /// diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index b042841c..5423b612 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -108,6 +108,25 @@ public readonly record struct CommandLineParsingOptions }, }; + /// + /// 内部使用。当发现命令行参数只有一个,且符合 URL 格式时,无论用户设置了哪种命令行风格,都会使用此风格进行解析。 + /// + internal static CommandLineStyleDetails UrlStyle => new CommandLineStyleDetails + { + CaseSensitive = false, + SupportsLongOption = true, + SupportsShortOption = false, + SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = false, + SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = false, + SupportsSpaceSeparatedCollectionValues = false, + NamingPolicy = CommandNamingPolicy.Both, + OptionPrefix = CommandOptionPrefix.DoubleDash, + OptionValueSeparators = CommandSeparatorChars.Create('='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), + }; + /// /// 详细设置命令行解析时的各种细节。 /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 7f3d50e5..de9bdef7 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -24,7 +24,10 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int _commandLine = commandLine; _commandObjectName = commandObjectName; _commandCount = commandCount; - Style = commandLine.ParsingOptions.Style; + var isUrl = commandLine.MatchedUrlScheme is null; + Style = isUrl + ? commandLine.ParsingOptions.Style + : CommandLineParsingOptions.UrlStyle; _namingPolicy = Style.NamingPolicy; OptionPrefix = Style.OptionPrefix; _caseSensitive = Style.CaseSensitive; diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs new file mode 100644 index 00000000..327569bf --- /dev/null +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs @@ -0,0 +1,132 @@ +namespace DotNetCampus.Cli.Utils.Parsers; + +/// +/// 如果命令行参数中传入的是 URL,则尝试将其转换为普通的命令行参数列表。 +/// +internal static class CommandUrlParser +{ + /// + /// 尝试将命令行参数中的 URL 转换为普通的命令行参数列表。 + /// + /// 原始传入的命令行参数。 + /// 命令行解析选项。 + /// 如果传入的命令行参数中包含 URL,则返回转换后的命令行参数列表和 URL 的 Scheme 部分。 + internal static (string? MatchedUrlScheme, IReadOnlyList? UrlNormalizedArguments) TryNormalizeUrlArguments( + IReadOnlyList originalArguments, CommandLineParsingOptions options) + { + if (originalArguments.Count is not 1 || options.SchemeNames is not { Count: > 0 } schemeNames) + { + return (null, null); + } + + var argument = originalArguments[0]; + foreach (var schemeName in schemeNames) + { + if (argument.StartsWith($"{schemeName}://", StringComparison.OrdinalIgnoreCase)) + { + return (schemeName, NormalizeUrlArguments(schemeName, argument)); + } + } + return (null, null); + } + + /// + /// 将 URL 转换为普通的命令行参数列表。
+ ///
+ /// URL 的 Scheme 部分。 + /// URL 字符串。 + /// 普通的命令行参数列表。 + private static IReadOnlyList NormalizeUrlArguments(string schema, string argument) + { + // schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 + + var span = argument.AsSpan(); + + // 1. 跳过 schema:// + span = span[(schema.Length + 3)..]; + + // 2. 分成三个部分,分别解析。 + var questionMarkIndex = span.IndexOf('?'); + var fragmentIndex = span.IndexOf('#'); + var commandAndPositionalArgumentSpan = questionMarkIndex switch + { + -1 when fragmentIndex == -1 => span, + -1 => span[..fragmentIndex], + _ when fragmentIndex == -1 => span[..questionMarkIndex], + _ => span[..Math.Min(questionMarkIndex, fragmentIndex)], + }; + var optionSpan = questionMarkIndex switch + { + -1 => [], + _ when fragmentIndex == -1 => span[(questionMarkIndex + 1)..], + _ => span[(questionMarkIndex + 1)..fragmentIndex], + }; + var fragmentSpan = fragmentIndex switch + { + -1 => [], + _ => span[(fragmentIndex + 1)..], + }; + + // 3. 解析各个部分。 + var commandAndPositionalArgumentList = ParseCommandAndPositionalArguments(commandAndPositionalArgumentSpan); + var optionList = ParseOptions(optionSpan); + var fragmentList = ParseFragment(fragmentSpan); + + return [..commandAndPositionalArgumentList, ..optionList, ..fragmentList]; + } + + private static IReadOnlyList ParseCommandAndPositionalArguments(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + var parts = argument.ToString().Split(['/'], StringSplitOptions.RemoveEmptyEntries); + var result = new List(parts.Length); + foreach (var part in parts) + { + result.Add(Uri.UnescapeDataString(part)); + } + return result; + } + + private static IReadOnlyList ParseOptions(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + var parts = argument.ToString().Split(['&'], StringSplitOptions.RemoveEmptyEntries); + var result = new List(parts.Length); + foreach (var part in parts) + { + var equalSignIndex = part.IndexOf('='); + if (equalSignIndex == -1) + { + // 只有键,没有值 + result.Add($"--{Uri.UnescapeDataString(part)}"); + } + else + { + var key = part[..equalSignIndex]; + var value = part[(equalSignIndex + 1)..]; + result.Add($"--{Uri.UnescapeDataString(key)}={Uri.UnescapeDataString(value)}"); + } + } + + return result; + } + + private static IReadOnlyList ParseFragment(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + // 片段部分直接作为一个位置参数 + return ["--fragment", Uri.UnescapeDataString(argument.ToString())]; + } +} diff --git a/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs index 6de6c873..58bf82ca 100644 --- a/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs @@ -41,7 +41,7 @@ public void CompleteUrl_WithSchemePathAndQuery_ParsedCorrectly() .Run(); // Assert - Assert.AreEqual("documents/open", path); + Assert.AreEqual("open", path); Assert.IsTrue(readOnly); Assert.AreEqual("yes", highlight); } @@ -541,12 +541,14 @@ internal record Url04_CaseInsensitiveSchemeOptions public string Param { get; init; } = string.Empty; } +[Command("path")] internal record Url05_BasicQueryParamOptions { [Option] public string Name { get; init; } = string.Empty; } +[Command("path")] internal record Url06_MultipleQueryParamOptions { [Option] @@ -559,6 +561,7 @@ internal record Url06_MultipleQueryParamOptions public string Location { get; init; } = string.Empty; } +[Command("path")] internal record Url07_BooleanQueryParamOptions { [Option] @@ -568,6 +571,7 @@ internal record Url07_BooleanQueryParamOptions public bool Verbose { get; init; } } +[Command("path")] internal record Url08_EmptyValueQueryParamOptions { [Option] @@ -577,12 +581,14 @@ internal record Url08_EmptyValueQueryParamOptions public string Comment { get; init; } = string.Empty; } +[Command("path")] internal record Url09_ArrayQueryParamOptions { [Option] public string[] Tags { get; init; } = []; } +[Command("path")] internal record Url10_IntegerTypeOptions { [Option] @@ -592,6 +598,7 @@ internal record Url10_IntegerTypeOptions public int Count { get; init; } } +[Command("path")] internal record Url11_BooleanTypeOptions { [Option] @@ -604,6 +611,7 @@ internal record Url11_BooleanTypeOptions public bool Flag { get; init; } } +[Command("path")] internal record Url12_EnumTypeOptions { /// @@ -619,6 +627,7 @@ internal record Url12_EnumTypeOptions public CommandLineStyle Style { get; init; } } +[Command("path")] internal record Url13_CollectionTypeOptions { [Option] @@ -628,6 +637,7 @@ internal record Url13_CollectionTypeOptions public IReadOnlyList Names { get; init; } = []; } +[Command("path")] internal record Url14_UrlEncodedOptions { [Option] @@ -637,12 +647,14 @@ internal record Url14_UrlEncodedOptions public string Path { get; init; } = string.Empty; } +[Command("path")] internal record Url15_SpecialCharsOptions { [Option] public string Special { get; init; } = string.Empty; } +[Command("path")] internal record Url16_NonAsciiOptions { [Option] @@ -680,12 +692,14 @@ internal record Url20_MalformedUrlOptions public string Value { get; init; } = string.Empty; } +[Command("path")] internal record Url21_DuplicateParamOptions { [Option] public string Name { get; init; } = string.Empty; } +[Command("path")] internal record Url22_FragmentOptions { [Option] From a7fbd9330005ff9da344148ee661fa94e6150995 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 17:56:10 +0800 Subject: [PATCH 039/193] =?UTF-8?q?=E6=95=B4=E7=90=86=20Benchmark=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParserTest.cs | 65 +++++++++++++++---- .../Fakes/CommandLineArgs.cs | 20 +++++- .../Fakes/Options.cs | 14 ++-- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs b/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs index a251bf9d..78fcbf0e 100644 --- a/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs +++ b/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs @@ -12,7 +12,7 @@ namespace DotNetCampus.Cli.Performance; -// [DryJob] // 取消注释以验证测试能否运行。 +// [DryJob] [MemoryDiagnoser] [BenchmarkCategory("CommandLine.Parse")] public class CommandLineParserTest @@ -66,31 +66,59 @@ public void Parse_NoArgs_3x_Runtime() commandLine.As(); } + [Benchmark(Description = "parse [NET] --flexible")] + public void Parse_DotNet_Flexible() + { + var commandLine = CommandLine.Parse(DotNetStyleArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [NET] --dotnet")] + public void Parse_DotNet_PowerShell() + { + var commandLine = CommandLine.Parse(DotNetStyleArgs, DotNet); + commandLine.As(); + } + + [Benchmark(Description = "parse [NET] -v=3.x -p=parser")] + public void Parse_DotNet_3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetStyleArgs); + commandLine.As(new OptionsParser()); + } + + [Benchmark(Description = "parse [NET] -v=3.x -p=runtime")] + public void Parse_DotNet_3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetStyleArgs); + commandLine.As(); + } + [Benchmark(Description = "parse [PS1] --flexible")] public void Parse_PowerShell_Flexible() { - var commandLine = CommandLine.Parse(WindowsStyleArgs, Flexible); + var commandLine = CommandLine.Parse(PowerShellStyleArgs, Flexible); commandLine.As(); } - [Benchmark(Description = "parse [PS1] --dotnet")] + [Benchmark(Description = "parse [PS1] --powershell")] public void Parse_PowerShell_PowerShell() { - var commandLine = CommandLine.Parse(WindowsStyleArgs, DotNet); + var commandLine = CommandLine.Parse(PowerShellStyleArgs, PowerShell); commandLine.As(); } [Benchmark(Description = "parse [PS1] -v=3.x -p=parser")] public void Parse_PowerShell_3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(WindowsStyleArgs); + var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellStyleArgs); commandLine.As(new OptionsParser()); } [Benchmark(Description = "parse [PS1] -v=3.x -p=runtime")] public void Parse_PowerShell_3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(WindowsStyleArgs); + var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellStyleArgs); commandLine.As(); } @@ -101,10 +129,10 @@ public void Parse_Cmd_Flexible() commandLine.As(); } - [Benchmark(Description = "parse [CMD] --dotnet")] + [Benchmark(Description = "parse [CMD] --powershell")] public void Parse_Cmd_PowerShell() { - var commandLine = CommandLine.Parse(CmdStyleArgs, DotNet); + var commandLine = CommandLine.Parse(CmdStyleArgs, PowerShell); commandLine.As(); } @@ -125,28 +153,28 @@ public void Parse_Cmd_3x_Runtime() [Benchmark(Description = "parse [GNU] --flexible")] public void Parse_Gnu_Flexible() { - var commandLine = CommandLine.Parse(LinuxStyleArgs, Flexible); + var commandLine = CommandLine.Parse(GnuStyleArgs, Flexible); commandLine.As(); } [Benchmark(Description = "parse [GNU] --gnu")] public void Parse_Gnu_Gnu() { - var commandLine = CommandLine.Parse(LinuxStyleArgs, Gnu); + var commandLine = CommandLine.Parse(GnuStyleArgs, Gnu); commandLine.As(); } [Benchmark(Description = "parse [GNU] -v=3.x -p=parser")] public void Parse_Gnu_3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(LinuxStyleArgs); + var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuStyleArgs); commandLine.As(new OptionsParser()); } [Benchmark(Description = "parse [GNU] -v=3.x -p=runtime")] public void Parse_Gnu_3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(LinuxStyleArgs); + var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuStyleArgs); commandLine.As(); } @@ -159,6 +187,15 @@ public void Handle_Verbs_Flexible() .Run(); } + [Benchmark(Description = "handle [Edit,Print] --dotnet")] + public void Handle_Verbs_DotNet() + { + CommandLine.Parse(EditVerbArgs) + .AddHandler(options => 0) + .AddHandler(options => 0) + .Run(); + } + [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] public void Handle_Verbs_Parser() { @@ -203,7 +240,7 @@ public void Parse_Url_3x_Runtime() [Benchmark(Description = "NuGet: CommandLineParser")] public void CommandLineParser() { - Parser.Default.ParseArguments(LinuxStyleArgs).WithParsed(options => { }); + Parser.Default.ParseArguments(GnuStyleArgs).WithParsed(options => { }); } [Benchmark(Description = "NuGet: System.CommandLine")] @@ -217,6 +254,6 @@ public void SystemCommandLine() rootCommand.AddOption(fileOption); rootCommand.SetHandler(file => { }, fileOption); - rootCommand.Invoke(LinuxStyleArgs); + rootCommand.Invoke(GnuStyleArgs); } } diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs index 203000bc..63f89e69 100644 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs +++ b/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs @@ -11,9 +11,23 @@ internal static class CommandLineArgs internal const string PlacementValue = "Outside"; internal const string StartupSessionValue = "89EA9D26-6464-4E71-BD04-AA6516063D83"; - internal static readonly string[] NoArgs = new string[0]; + internal static readonly string[] NoArgs = []; - internal static readonly string[] WindowsStyleArgs = + internal static readonly string[] DotNetStyleArgs = + { + FileValue, + "--cloud", + "--iwb", + "-m", + ModeValue, + "-s", + "-p", + PlacementValue, + "--startup-session", + StartupSessionValue, + }; + + internal static readonly string[] PowerShellStyleArgs = { FileValue, "-Cloud", @@ -52,7 +66,7 @@ internal static class CommandLineArgs $"/StartupSession:{StartupSessionValue}", }; - internal static readonly string[] LinuxStyleArgs = + internal static readonly string[] GnuStyleArgs = { FileValue, "--cloud", diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs index fa7e8607..08f7e2df 100644 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs +++ b/tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs @@ -10,49 +10,49 @@ public class Options /// /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 /// - [DotNetCampus.Cli.Compiler.Value(0), DotNetCampus.Cli.Compiler.Option('f', "File")] + [DotNetCampus.Cli.Compiler.Value(0), DotNetCampus.Cli.Compiler.Option('f', "file")] [dotnetCampus.Cli.Value(0), dotnetCampus.Cli.Option('f', "File")] public string? FilePath { get; set; } /// /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 /// - [DotNetCampus.Cli.Compiler.Option("Cloud")] + [DotNetCampus.Cli.Compiler.Option("cloud")] [dotnetCampus.Cli.Option("Cloud"), DefaultValue(false)] public bool IsFromCloud { get; init; } /// /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 /// - [DotNetCampus.Cli.Compiler.Option('m', "Mode")] + [DotNetCampus.Cli.Compiler.Option('m', "mode")] [dotnetCampus.Cli.Option('m', "Mode")] public string? StartupMode { get; init; } /// /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 /// - [DotNetCampus.Cli.Compiler.Option('s', "Silence")] + [DotNetCampus.Cli.Compiler.Option('s', "silence")] [dotnetCampus.Cli.Option('s', "Silence"), DefaultValue(false)] public bool IsSilence { get; init; } /// /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 /// - [DotNetCampus.Cli.Compiler.Option("Iwb")] + [DotNetCampus.Cli.Compiler.Option("iwb")] [dotnetCampus.Cli.Option("Iwb"), DefaultValue(false)] public bool IsIwb { get; init; } /// /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 /// - [DotNetCampus.Cli.Compiler.Option('p', "Placement")] + [DotNetCampus.Cli.Compiler.Option('p', "placement")] [dotnetCampus.Cli.Option('p', "Placement")] public string? Placement { get; init; } /// /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 /// - [DotNetCampus.Cli.Compiler.Option("StartupSession")] + [DotNetCampus.Cli.Compiler.Option("startup-session")] [dotnetCampus.Cli.Option("StartupSession")] public string? StartupSession { get; init; } From 8a535e32080e9de3cf49637f7587bb27ee65f660 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 19:50:11 +0800 Subject: [PATCH 040/193] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Program.cs | 97 +++++---- .../CommandLineParsingOptions.cs | 203 +++++++++++++----- .../CommandSeparatorChars.cs | 80 ++++--- .../Utils/BooleanValues16.cs | 9 + .../Utils/Parsers/CommandLineParser.cs | 12 +- .../CommandLineParsingOptionsTests.cs | 15 ++ 6 files changed, 260 insertions(+), 156 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs diff --git a/samples/DotNetCampus.CommandLine.Sample/Program.cs b/samples/DotNetCampus.CommandLine.Sample/Program.cs index 524c73a0..4260489a 100644 --- a/samples/DotNetCampus.CommandLine.Sample/Program.cs +++ b/samples/DotNetCampus.CommandLine.Sample/Program.cs @@ -1,5 +1,4 @@ #define Benchmark -using System.Data.Common; using System.Diagnostics; using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; @@ -11,7 +10,7 @@ class Program { static void Main(string[] args) { -#if Benchmark +#if !Benchmark // 第一次运行,排除类型初始化的影响,只测试代码执行性能。 // 注释掉这句话,可以: // 1. 测试带类型初始化的性能 @@ -22,10 +21,11 @@ static void Main(string[] args) stopwatch.Stop(); Console.WriteLine($"[# Elapsed: {stopwatch.Elapsed.TotalMicroseconds} us #]"); #else - const int testCount = 1000000; + const int warmupCount = 10000; + const int testCount = 10000000; CommandLineParsingOptions parsingOptions = CommandLineParsingOptions.DotNet; - for (var i = 0; i < testCount; i++) + for (var i = 0; i < warmupCount; i++) { dotnetCampus.Cli.CommandLine.Parse(args).As(new OptionsParser()); dotnetCampus.Cli.CommandLine.Parse(args).As(); @@ -39,53 +39,56 @@ static void Main(string[] args) Console.WriteLine("| Version | Parse | As(Parser) | As(Runtime) |"); Console.WriteLine("| ------- | ------- | ---------- | ----------- |"); - Console.Write("| 3.x | "); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) { - _ = dotnetCampus.Cli.CommandLine.Parse(args); + Console.Write("| 3.x | "); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = dotnetCampus.Cli.CommandLine.Parse(args); + } + stopwatch.Stop(); + Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); + var oldCommandLine = dotnetCampus.Cli.CommandLine.Parse(args); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = oldCommandLine.As(new OptionsParser()); + } + stopwatch.Stop(); + Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = oldCommandLine.As(); + } + stopwatch.Stop(); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); } - stopwatch.Stop(); - Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); - var oldCommandLine = dotnetCampus.Cli.CommandLine.Parse(args); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) - { - _ = oldCommandLine.As(new OptionsParser()); - } - stopwatch.Stop(); - Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) - { - _ = oldCommandLine.As(); - } - stopwatch.Stop(); - Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); - - Console.Write("| 4.x | "); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) { - _ = CommandLine.Parse(args, parsingOptions); + Console.Write("| 4.x | "); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = CommandLine.Parse(args, parsingOptions); + } + stopwatch.Stop(); + Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); + var newCommandLine = CommandLine.Parse(args, parsingOptions); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = newCommandLine.As(OptionsBuilder.CreateInstance); + } + stopwatch.Stop(); + Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); + stopwatch.Restart(); + for (var i = 0; i < testCount; i++) + { + _ = newCommandLine.As(); + } + stopwatch.Stop(); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); } - stopwatch.Stop(); - Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); - var newCommandLine = CommandLine.Parse(args, parsingOptions); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) - { - _ = newCommandLine.As(OptionsBuilder.CreateInstance); - } - stopwatch.Stop(); - Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); - stopwatch.Restart(); - for (var i = 0; i < testCount; i++) - { - _ = newCommandLine.As(); - } - stopwatch.Stop(); - Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); #endif } diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 5423b612..01d76b1d 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.Contracts; using DotNetCampus.Cli.Utils; namespace DotNetCampus.Cli; @@ -10,108 +11,139 @@ public readonly record struct CommandLineParsingOptions /// public static CommandLineParsingOptions Flexible => new CommandLineParsingOptions { - Style = new CommandLineStyleDetails + Style = new CommandLineStyleDetails(FlexibleMagic) { - CaseSensitive = false, - SupportsLongOption = true, - SupportsShortOption = true, - SupportsShortOptionCombination = false, - SupportsMultiCharShortOption = false, - SupportsShortOptionValueWithoutSeparator = false, - SupportsSpaceSeparatedOptionValue = true, - SupportsSpaceSeparatedCollectionValues = true, - NamingPolicy = CommandNamingPolicy.Both, - OptionPrefix = CommandOptionPrefix.Any, OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; + private static CommandLineStyleDetails FlexibleDefinition => new CommandLineStyleDetails + { + CaseSensitive = false, + SupportsLongOption = true, + SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = false, + SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, + NamingPolicy = CommandNamingPolicy.Both, + OptionPrefix = CommandOptionPrefix.Any, + }; + /// public static CommandLineParsingOptions DotNet => new CommandLineParsingOptions { - Style = new CommandLineStyleDetails + Style = new CommandLineStyleDetails(DotNetMagic) { - CaseSensitive = true, - SupportsLongOption = true, - SupportsShortOption = true, - SupportsShortOptionCombination = false, - SupportsMultiCharShortOption = true, - SupportsShortOptionValueWithoutSeparator = false, - SupportsSpaceSeparatedOptionValue = true, - SupportsSpaceSeparatedCollectionValues = true, - NamingPolicy = CommandNamingPolicy.KebabCase, - OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; + private static CommandLineStyleDetails DotNetDefinition => new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = true, + SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = true, + SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, + NamingPolicy = CommandNamingPolicy.KebabCase, + OptionPrefix = CommandOptionPrefix.DoubleDash, + }; + /// public static CommandLineParsingOptions Gnu => new CommandLineParsingOptions { - Style = new CommandLineStyleDetails + Style = new CommandLineStyleDetails(GnuMagic) { - CaseSensitive = true, - SupportsLongOption = true, - SupportsShortOption = true, - SupportsShortOptionCombination = true, - SupportsMultiCharShortOption = false, - SupportsShortOptionValueWithoutSeparator = true, - SupportsSpaceSeparatedOptionValue = true, - SupportsSpaceSeparatedCollectionValues = false, - NamingPolicy = CommandNamingPolicy.KebabCase, - OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create('='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; + private static CommandLineStyleDetails GnuDefinition => new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = true, + SupportsShortOption = true, + SupportsShortOptionCombination = true, + SupportsMultiCharShortOption = false, + SupportsShortOptionValueWithoutSeparator = true, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = false, + NamingPolicy = CommandNamingPolicy.KebabCase, + OptionPrefix = CommandOptionPrefix.DoubleDash, + }; + /// public static CommandLineParsingOptions Posix => new CommandLineParsingOptions { - Style = new CommandLineStyleDetails + Style = new CommandLineStyleDetails(PosixMagic) { - CaseSensitive = true, - SupportsLongOption = false, - SupportsShortOption = true, - SupportsShortOptionCombination = true, - SupportsMultiCharShortOption = false, - SupportsShortOptionValueWithoutSeparator = false, - SupportsSpaceSeparatedOptionValue = true, - SupportsSpaceSeparatedCollectionValues = true, - NamingPolicy = CommandNamingPolicy.PascalCase, - // Posix 不支持长选项,使用 DoubleDash 的含义是 '-' 一定表示短选项。 - OptionPrefix = CommandOptionPrefix.DoubleDash, OptionValueSeparators = CommandSeparatorChars.Create(), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; + private static CommandLineStyleDetails PosixDefinition => new CommandLineStyleDetails + { + CaseSensitive = true, + SupportsLongOption = false, + SupportsShortOption = true, + SupportsShortOptionCombination = true, + SupportsMultiCharShortOption = false, + SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, + NamingPolicy = CommandNamingPolicy.PascalCase, + // Posix 不支持长选项,使用 DoubleDash 的含义是 '-' 一定表示短选项。 + OptionPrefix = CommandOptionPrefix.DoubleDash, + }; + + /// public static CommandLineParsingOptions PowerShell => new CommandLineParsingOptions { - Style = new CommandLineStyleDetails + Style = new CommandLineStyleDetails(PowerShellMagic) { - CaseSensitive = false, - SupportsLongOption = true, - SupportsShortOption = true, - SupportsShortOptionCombination = false, - SupportsMultiCharShortOption = true, - SupportsShortOptionValueWithoutSeparator = false, - SupportsSpaceSeparatedOptionValue = true, - SupportsSpaceSeparatedCollectionValues = true, - NamingPolicy = CommandNamingPolicy.PascalCase, - OptionPrefix = CommandOptionPrefix.SlashOrDash, OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, }; + /// + private static CommandLineStyleDetails PowerShellDefinition => new CommandLineStyleDetails + { + CaseSensitive = false, + SupportsLongOption = true, + SupportsShortOption = true, + SupportsShortOptionCombination = false, + SupportsMultiCharShortOption = true, + SupportsShortOptionValueWithoutSeparator = false, + SupportsSpaceSeparatedOptionValue = true, + SupportsSpaceSeparatedCollectionValues = true, + NamingPolicy = CommandNamingPolicy.PascalCase, + OptionPrefix = CommandOptionPrefix.SlashOrDash, + }; + + /// + /// 内部使用。当发现命令行参数只有一个,且符合 URL 格式时,无论用户设置了哪种命令行风格,都会使用此风格进行解析。 + /// + public static CommandLineStyleDetails UrlStyle => new CommandLineStyleDetails(UrlMagic) + { + OptionValueSeparators = CommandSeparatorChars.Create('='), + CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), + }; + /// /// 内部使用。当发现命令行参数只有一个,且符合 URL 格式时,无论用户设置了哪种命令行风格,都会使用此风格进行解析。 /// - internal static CommandLineStyleDetails UrlStyle => new CommandLineStyleDetails + private static CommandLineStyleDetails UrlDefinition => new CommandLineStyleDetails { CaseSensitive = false, SupportsLongOption = true, @@ -123,8 +155,6 @@ public readonly record struct CommandLineParsingOptions SupportsSpaceSeparatedCollectionValues = false, NamingPolicy = CommandNamingPolicy.Both, OptionPrefix = CommandOptionPrefix.DoubleDash, - OptionValueSeparators = CommandSeparatorChars.Create('='), - CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }; /// @@ -179,6 +209,54 @@ public readonly record struct CommandLineParsingOptions /// /// public IReadOnlyList? SchemeNames { get; init; } + + private const ushort FlexibleMagic = 0x18C7; + private const ushort DotNetMagic = 0x1AE1; + private const ushort GnuMagic = 0xDE1; + private const ushort PosixMagic = 0x19A2; + private const ushort PowerShellMagic = 0x1ADA; + private const ushort UrlMagic = 0x0043; + +#if DEBUG + + /// + /// 在单元测试里调用,以验证各种预定义的命令行风格没有被意外修改。 + /// + public static void VerifyMagicNumbers() + { + var flexibleMagic = FlexibleDefinition.GetMagicNumber(); + var dotNetMagic = DotNetDefinition.GetMagicNumber(); + var gnuMagic = GnuDefinition.GetMagicNumber(); + var posixMagic = PosixDefinition.GetMagicNumber(); + var powerShellMagic = PowerShellDefinition.GetMagicNumber(); + var urlMagic = UrlDefinition.GetMagicNumber(); + if (flexibleMagic != FlexibleMagic) + { + throw new InvalidOperationException($"The new magic number of Flexible is 0x{flexibleMagic:X4}."); + } + if (dotNetMagic != DotNetMagic) + { + throw new InvalidOperationException($"The new magic number of DotNet is 0x{dotNetMagic:X4}."); + } + if (gnuMagic != GnuMagic) + { + throw new InvalidOperationException($"The new magic number of Gnu is 0x{gnuMagic:X4}."); + } + if (posixMagic != PosixMagic) + { + throw new InvalidOperationException($"The new magic number of Posix is 0x{posixMagic:X4}."); + } + if (powerShellMagic != PowerShellMagic) + { + throw new InvalidOperationException($"The new magic number of PowerShell is 0x{powerShellMagic:X4}."); + } + if (urlMagic != UrlMagic) + { + throw new InvalidOperationException($"The new magic number of UrlStyle is 0x{urlMagic:X4}."); + } + } + +#endif } /// @@ -351,6 +429,13 @@ public bool SupportsSpaceSeparatedCollectionValues /// 如 ',', ';', ' ' 分别对应: --option value1,value2, --option value1;value2, --option value1 value2。 /// public CommandSeparatorChars CollectionValueSeparators { get; init; } + + /// + /// 获取用于存储样式细节的魔术数字。 + /// + /// 魔术数字。 + [Pure] + internal ushort GetMagicNumber() => _booleans.GetMagicNumber(); } /// diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs index 699d741f..ff2b6ad4 100644 --- a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs @@ -4,8 +4,7 @@ namespace DotNetCampus.Cli; /// -/// 允许用户在命令行中使用的分隔符字符集合。
-/// 用节省空间的方式存储不小于长度 4 的多个字符。 +/// 允许用户在命令行中使用的分隔符字符集合。最多只能支持 个字符。 ///
#if NET8_0_OR_GREATER [CollectionBuilder(typeof(CommandSeparatorChars), nameof(Create))] @@ -13,13 +12,18 @@ namespace DotNetCampus.Cli; public readonly record struct CommandSeparatorChars : IEnumerable { /// - /// 最多支持 4 个分隔符字符。 + /// 分隔符字符集合中允许的最大字符数量。 /// - private readonly uint _chars; + internal const int MaxSupportedCount = 2; - private CommandSeparatorChars(uint packedChars) + private readonly char _char0; + + private readonly char _char1; + + private CommandSeparatorChars(char char0, char char1) { - _chars = packedChars; + _char0 = char0; + _char1 = char1; } /// @@ -28,19 +32,20 @@ private CommandSeparatorChars(uint packedChars) /// 分隔符字符集合。 public void CopyTo(Span buffer, out int length) { - length = 0; - var packed = _chars; - while (packed != 0) + if (_char0 is '\0') { - var c = (char)(packed & 0xFF); - if (length < buffer.Length) - { - buffer[length] = c; - } - - length++; - packed >>= 8; + length = 0; + return; + } + if (_char1 is '\0') + { + buffer[0] = _char0; + length = 1; + return; } + buffer[0] = _char0; + buffer[1] = _char1; + length = 2; } /// @@ -49,44 +54,31 @@ public void CopyTo(Span buffer, out int length) /// 一个可用于遍历 中字符的枚举器。 public IEnumerator GetEnumerator() { - var packed = _chars; - while (packed != 0) + if (_char0 is not '\0') + { + yield return _char0; + } + if (_char1 is not '\0') { - var c = (char)(packed & 0xFF); - yield return c; - packed >>= 8; + yield return _char1; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// - /// 从长度不大于 4 的字符(ASCII)集合创建一个新的 实例。 + /// 从长度不大于 的字符(ASCII)集合创建一个新的 实例。 /// /// 分隔符字符集合。 /// 新的 实例。 - /// 如果 长度大于 4。 + /// 如果 长度大于 /// 如果 中包含 null 字符。 [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static CommandSeparatorChars Create(params ReadOnlySpan chars) + public static CommandSeparatorChars Create(params ReadOnlySpan chars) => chars.Length switch { - if (chars.Length > 4) - { - throw new ArgumentOutOfRangeException(nameof(chars), "最多只能指定 4 个分隔符字符。"); - } - - uint packed = 0; - for (var i = chars.Length - 1; i >= 0; i--) - { - var c = chars[i]; - if (c == 0) - { - throw new ArgumentException("不支持 null 字符作为分隔符。", nameof(chars)); - } - - packed = (packed << 8) | c; - } - - return new CommandSeparatorChars(packed); - } + 0 => new CommandSeparatorChars('\0', '\0'), + 1 => new CommandSeparatorChars(chars[0], '\0'), + 2 => new CommandSeparatorChars(chars[0], chars[1]), + _ => throw new ArgumentOutOfRangeException(nameof(chars), $"The length of chars cannot be greater than {MaxSupportedCount}."), + }; } diff --git a/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs b/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs index ae93a34e..5fe10942 100644 --- a/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs +++ b/src/DotNetCampus.CommandLine/Utils/BooleanValues16.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.Contracts; + namespace DotNetCampus.Cli.Utils; /// @@ -70,4 +72,11 @@ internal bool this[int index] _value = (ushort)((_value & ~(7 << index)) | (bits << index)); } } + + /// + /// 获取用于存储布尔值的魔术数字。 + /// + /// 魔术数字。 + [Pure] + internal ushort GetMagicNumber() => _value; } diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index de9bdef7..229c9f5e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -272,7 +272,7 @@ private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadO var result = CommandLineParsingResult.Success; if (match.ValueType is OptionValueType.List) { - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; Style.CollectionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; @@ -297,7 +297,7 @@ private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadO } else if (match.ValueType is OptionValueType.Dictionary) { - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; Style.CollectionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; @@ -541,7 +541,7 @@ private bool ParseOptionOrPositionalArgument() if (argument.Length is 1) { // 单个字符,确定一下是否是选项分隔符,如果是则要报错。 - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; if (argument.IndexOfAny(separators) >= 0) @@ -591,7 +591,7 @@ private bool ParseOptionOrPositionalArgument() private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) { - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; @@ -618,7 +618,7 @@ private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) { - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; @@ -659,7 +659,7 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) { - Span separators = stackalloc char[4]; + Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); separators = separators[..length]; diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs new file mode 100644 index 00000000..0bd2db77 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs @@ -0,0 +1,15 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests; + +[TestClass] +public class CommandLineParsingOptionsTests +{ +#if DEBUG + [TestMethod("魔法数字必须严格和实际样式匹配")] + public void MagicNumber_MustMatchRealStyle() + { + CommandLineParsingOptions.VerifyMagicNumbers(); + } +#endif +} From ae6c22d00621d572b1f3729eb9a907162e0cd4fc Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 16 Sep 2025 22:59:31 +0800 Subject: [PATCH 041/193] =?UTF-8?q?=E6=95=B4=E7=90=86=20Benchmark=20?= =?UTF-8?q?=E7=9A=84=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParserTest.cs | 259 ------------------ ...otNetCampus.CommandLine.Performance.csproj | 5 - .../Fakes/BenchmarkOptions3.cs | 134 +++++++++ .../Fakes/BenchmarkOptions4.cs | 46 ++++ .../Fakes/CommandLineArguments.cs | 61 +++++ .../Fakes/ComparedOptions.cs | 52 ---- .../Fakes/DetailLevel.cs | 8 + .../Others.cs | 93 +++++++ .../ParseArgs/ParseCmdArgs.cs | 41 +++ .../ParseArgs/ParseDotNetArgs.cs | 41 +++ .../ParseArgs/ParseGnuArgs.cs | 72 +++++ .../ParseArgs/ParseMixArgs.cs | 34 +++ .../ParseArgs/ParseNoArgs.cs | 41 +++ .../ParseArgs/ParsePowerShellArgs.cs | 41 +++ .../Program.cs | 23 +- 15 files changed, 634 insertions(+), 317 deletions(-) delete mode 100644 tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions3.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs delete mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/ComparedOptions.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/DetailLevel.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/Others.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs create mode 100644 tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs diff --git a/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs b/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs deleted file mode 100644 index 78fcbf0e..00000000 --- a/tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.CommandLine; -using System.IO; -using BenchmarkDotNet.Attributes; -using CommandLine; -using dotnetCampus.Cli; -using DotNetCampus.Cli.Performance.Fakes; -using DotNetCampus.Cli.Tests.Fakes; -using static DotNetCampus.Cli.Tests.Fakes.CommandLineArgs; -using static DotNetCampus.Cli.CommandLineParsingOptions; - -// ReSharper disable ReturnValueOfPureMethodIsNotUsed - -namespace DotNetCampus.Cli.Performance; - -// [DryJob] -[MemoryDiagnoser] -[BenchmarkCategory("CommandLine.Parse")] -public class CommandLineParserTest -{ - [Benchmark(Description = "parse [] --flexible")] - public void Parse_NoArgs_Flexible() - { - var commandLine = CommandLine.Parse(NoArgs, Flexible); - commandLine.As(); - } - - [Benchmark(Description = "parse [] --gnu")] - public void Parse_NoArgs_Gnu() - { - var commandLine = CommandLine.Parse(NoArgs, Gnu); - commandLine.As(); - } - - [Benchmark(Description = "parse [] --posix")] - public void Parse_NoArgs_Posix() - { - var commandLine = CommandLine.Parse(NoArgs, Posix); - commandLine.As(); - } - - [Benchmark(Description = "parse [] --dotnet")] - public void Parse_NoArgs_DotNet() - { - var commandLine = CommandLine.Parse(NoArgs, DotNet); - commandLine.As(); - } - - [Benchmark(Description = "parse [] --powershell")] - public void Parse_NoArgs_PowerShell() - { - var commandLine = CommandLine.Parse(NoArgs, PowerShell); - commandLine.As(); - } - - [Benchmark(Description = "parse [] -v=3.x -p=parser")] - public void Parse_NoArgs_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [] -v=3.x -p=runtime")] - public void Parse_NoArgs_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); - commandLine.As(); - } - - [Benchmark(Description = "parse [NET] --flexible")] - public void Parse_DotNet_Flexible() - { - var commandLine = CommandLine.Parse(DotNetStyleArgs, Flexible); - commandLine.As(); - } - - [Benchmark(Description = "parse [NET] --dotnet")] - public void Parse_DotNet_PowerShell() - { - var commandLine = CommandLine.Parse(DotNetStyleArgs, DotNet); - commandLine.As(); - } - - [Benchmark(Description = "parse [NET] -v=3.x -p=parser")] - public void Parse_DotNet_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetStyleArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [NET] -v=3.x -p=runtime")] - public void Parse_DotNet_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetStyleArgs); - commandLine.As(); - } - - [Benchmark(Description = "parse [PS1] --flexible")] - public void Parse_PowerShell_Flexible() - { - var commandLine = CommandLine.Parse(PowerShellStyleArgs, Flexible); - commandLine.As(); - } - - [Benchmark(Description = "parse [PS1] --powershell")] - public void Parse_PowerShell_PowerShell() - { - var commandLine = CommandLine.Parse(PowerShellStyleArgs, PowerShell); - commandLine.As(); - } - - [Benchmark(Description = "parse [PS1] -v=3.x -p=parser")] - public void Parse_PowerShell_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellStyleArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [PS1] -v=3.x -p=runtime")] - public void Parse_PowerShell_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellStyleArgs); - commandLine.As(); - } - - [Benchmark(Description = "parse [CMD] --flexible")] - public void Parse_Cmd_Flexible() - { - var commandLine = CommandLine.Parse(CmdStyleArgs, Flexible); - commandLine.As(); - } - - [Benchmark(Description = "parse [CMD] --powershell")] - public void Parse_Cmd_PowerShell() - { - var commandLine = CommandLine.Parse(CmdStyleArgs, PowerShell); - commandLine.As(); - } - - [Benchmark(Description = "parse [CMD] -v=3.x -p=parser")] - public void Parse_Cmd_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdStyleArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [CMD] -v=3.x -p=runtime")] - public void Parse_Cmd_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdStyleArgs); - commandLine.As(); - } - - [Benchmark(Description = "parse [GNU] --flexible")] - public void Parse_Gnu_Flexible() - { - var commandLine = CommandLine.Parse(GnuStyleArgs, Flexible); - commandLine.As(); - } - - [Benchmark(Description = "parse [GNU] --gnu")] - public void Parse_Gnu_Gnu() - { - var commandLine = CommandLine.Parse(GnuStyleArgs, Gnu); - commandLine.As(); - } - - [Benchmark(Description = "parse [GNU] -v=3.x -p=parser")] - public void Parse_Gnu_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuStyleArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [GNU] -v=3.x -p=runtime")] - public void Parse_Gnu_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuStyleArgs); - commandLine.As(); - } - - [Benchmark(Description = "handle [Edit,Print] --flexible")] - public void Handle_Verbs_Flexible() - { - CommandLine.Parse(EditVerbArgs) - .AddHandler(options => 0) - .AddHandler(options => 0) - .Run(); - } - - [Benchmark(Description = "handle [Edit,Print] --dotnet")] - public void Handle_Verbs_DotNet() - { - CommandLine.Parse(EditVerbArgs) - .AddHandler(options => 0) - .AddHandler(options => 0) - .Run(); - } - - [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] - public void Handle_Verbs_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); - commandLine - .AddHandler(options => 0, new SelfWrittenEditOptionsParser()) - .AddHandler(options => 0, new SelfWrittenPrintOptionsParser()) - .Run(); - } - - [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=runtime")] - public void Handle_Verbs_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); - commandLine - .AddHandler(options => 0) - .AddHandler(options => 0) - .Run(); - } - - [Benchmark(Description = "parse [URL]")] - public void Parse_Url() - { - var commandLine = CommandLine.Parse(UrlArgs, new CommandLineParsingOptions { SchemeNames = ["walterlv"] }); - commandLine.As(); - } - - [Benchmark(Description = "parse [URL] -v=3.x -p=parser")] - public void Parse_Url_3x_Parser() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); - commandLine.As(new OptionsParser()); - } - - [Benchmark(Description = "parse [URL] -v=3.x -p=runtime")] - public void Parse_Url_3x_Runtime() - { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); - commandLine.As(); - } - - [Benchmark(Description = "NuGet: CommandLineParser")] - public void CommandLineParser() - { - Parser.Default.ParseArguments(GnuStyleArgs).WithParsed(options => { }); - } - - [Benchmark(Description = "NuGet: System.CommandLine")] - public void SystemCommandLine() - { - var fileOption = new System.CommandLine.Option( - name: "--file", - description: "The file to read and display on the console."); - - var rootCommand = new RootCommand("Benchmark for System.CommandLine"); - rootCommand.AddOption(fileOption); - rootCommand.SetHandler(file => { }, fileOption); - - rootCommand.Invoke(GnuStyleArgs); - } -} diff --git a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj index 0eec5c83..9058d43a 100644 --- a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj +++ b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj @@ -11,11 +11,6 @@ - - - - - diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions3.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions3.cs new file mode 100644 index 00000000..85a9263a --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions3.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using dotnetCampus.Cli; + +namespace DotNetCampus.Cli.Performance.Fakes; + +public class BenchmarkOptions3 +{ + [Option("Debug")] + public required bool IsDebugMode { get; init; } + + [Option('c', "Count")] + public required int TestCount { get; init; } + + [Option('n', "TestName")] + public string? TestName { get; set; } + + [Option("TestCategory")] + public string? TestCategory { get; set; } + + [Option('d', "DetailLevel")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; + + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} + +public class RuntimeBenchmarkOptions3 +{ + [Option("Debug")] + public required bool IsDebugMode { get; init; } + + [Option('c', "Count")] + public required int TestCount { get; init; } + + [Option('n', "TestName")] + public string? TestName { get; set; } + + [Option("TestCategory")] + public string? TestCategory { get; set; } + + [Option('d', "DetailLevel")] + public string DetailLevel { get; set; } = nameof(DotNetCampus.Cli.Performance.Fakes.DetailLevel.Medium); + + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} + +internal sealed class BenchmarkOption3Parser : ICommandLineOptionParser +{ + private bool _isDebugMode; + private int _testCount; + private string? _testName; + private string? _testCategory; + private DetailLevel _detailLevel = DetailLevel.Medium; + private List _testItems = new(); + + public string? Verb => null; + + public void SetValue(IReadOnlyList values) + { + _testItems = new List(values); + } + + public void SetValue(char shortName, bool value) + { + switch (shortName) + { + case 'd': + _isDebugMode = value; + break; + } + } + + public void SetValue(char shortName, string value) + { + switch (shortName) + { + case 'n': + _testName = value; + break; + case 'c': + _testCount = int.Parse(value); + break; + } + } + + public void SetValue(char shortName, IReadOnlyList values) + { + } + + public void SetValue(string longName, bool value) + { + switch (longName) + { + case "Debug": + _isDebugMode = value; + break; + } + } + + public void SetValue(string longName, string value) + { + switch (longName) + { + case "Count": + _testCount = int.Parse(value); + break; + case "TestName": + _testName = value; + break; + case "TestCategory": + _testCategory = value; + break; + case "DetailLevel": + _detailLevel = Enum.Parse(value); + break; + } + } + + public void SetValue(string longName, IReadOnlyList values) + { + } + + public BenchmarkOptions3 Commit() => new() + { + IsDebugMode = _isDebugMode, + TestCount = _testCount, + TestName = _testName, + TestCategory = _testCategory, + DetailLevel = _detailLevel, + TestItems = _testItems, + }; +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs new file mode 100644 index 00000000..99c4b910 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using DotNetCampus.Cli.Compiler; + +namespace DotNetCampus.Cli.Performance.Fakes; + +public class BenchmarkOptions4 +{ + [Option("debug")] + public required bool IsDebugMode { get; init; } + + [Option('c', "count")] + public required int TestCount { get; init; } + + [Option('n', "test-name")] + public string? TestName { get; set; } + + [Option("test-category")] + public string? TestCategory { get; set; } + + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; + + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} + +public class NullableBenchmarkOptions4 +{ + [Option("debug")] + public bool IsDebugMode { get; set; } + + [Option('c', "count")] + public int TestCount { get; set; } + + [Option('n', "test-name")] + public string? TestName { get; set; } + + [Option("test-category")] + public string? TestCategory { get; set; } + + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; + + [Value(0, int.MaxValue)] + public IList TestItems { get; set; } = null!; +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs new file mode 100644 index 00000000..8932ae45 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs @@ -0,0 +1,61 @@ +namespace DotNetCampus.Cli.Performance.Fakes; + +internal static class CommandLineArguments +{ + public static readonly string[] NoArgs = []; + + public static readonly string[] DotNetArgs = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "-c:20", + "--test-name:BenchmarkTest", + "--detail-level=High", + "--debug", + ]; + + public static readonly string[] PowerShellArgs = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "-c", "20", + "-TestName", "BenchmarkTest", + "-DetailLevel", "High", + "-Debug", + ]; + + public static readonly string[] CmdArgs = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "/c", "20", + "/TestName", "BenchmarkTest", + "/DetailLevel", "High", + "/Debug", + ]; + + public static readonly string[] GnuArgs = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "-c", "20", + "--test-name", "BenchmarkTest", + "--detail-level", "High", + "--debug", + ]; + + public static readonly string[] MixArgs = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "-c:20", + "/TestName", "BenchmarkTest", + "--detail-level=High", + "-Debug", + ]; +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/ComparedOptions.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/ComparedOptions.cs deleted file mode 100644 index e6511dd5..00000000 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/ComparedOptions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.ComponentModel; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Performance.Fakes; - -/// -/// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 -/// -public class ComparedOptions -{ - /// - /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Value(0), Option('f', "file")] - public string? FilePath { get; set; } - - /// - /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 - /// - [Option("cloud"), DefaultValue(false)] - public bool IsFromCloud { get; set; } - - /// - /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('m', "mode")] - public string? StartupMode { get; set; } - - /// - /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 - /// - [Option('s', "silence"), DefaultValue(false)] - public bool IsSilence { get; set; } - - /// - /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 - /// - [Option("iwb"), DefaultValue(false)] - public bool IsIwb { get; set; } - - /// - /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('p', "placement")] - public string? Placement { get; set; } - - /// - /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option("startup-session")] - public string? StartupSession { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/DetailLevel.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/DetailLevel.cs new file mode 100644 index 00000000..d8d185bb --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/DetailLevel.cs @@ -0,0 +1,8 @@ +namespace DotNetCampus.Cli.Performance.Fakes; + +public enum DetailLevel +{ + Low, + Medium, + High, +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Others.cs b/tests/DotNetCampus.CommandLine.Performance/Others.cs new file mode 100644 index 00000000..d9dd8a99 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Others.cs @@ -0,0 +1,93 @@ +using System.CommandLine; +using System.IO; +using BenchmarkDotNet.Attributes; +using dotnetCampus.Cli; +using Microsoft.Extensions.Options; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +namespace DotNetCampus.Cli.Performance; + +// [MemoryDiagnoser] +// [BenchmarkCategory("Parse No Args")] +public class Others +{ + // [Benchmark(Description = "handle [Edit,Print] --flexible")] + // public void Handle_Verbs_Flexible() + // { + // CommandLine.Parse(EditVerbArgs) + // .AddHandler(options => 0) + // .AddHandler(options => 0) + // .Run(); + // } + // + // [Benchmark(Description = "handle [Edit,Print] --dotnet")] + // public void Handle_Verbs_DotNet() + // { + // CommandLine.Parse(EditVerbArgs) + // .AddHandler(options => 0) + // .AddHandler(options => 0) + // .Run(); + // } + // + // [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] + // public void Handle_Verbs_Parser() + // { + // var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); + // commandLine + // .AddHandler(options => 0, new SelfWrittenEditOptionsParser()) + // .AddHandler(options => 0, new SelfWrittenPrintOptionsParser()) + // .Run(); + // } + // + // [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=runtime")] + // public void Handle_Verbs_Runtime() + // { + // var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); + // commandLine + // .AddHandler(options => 0) + // .AddHandler(options => 0) + // .Run(); + // } + // + // [Benchmark(Description = "parse [URL]")] + // public void Parse_Url() + // { + // var commandLine = CommandLine.Parse(UrlArgs, new CommandLineParsingOptions { SchemeNames = ["walterlv"] }); + // commandLine.As(); + // } + // + // [Benchmark(Description = "parse [URL] -v=3.x -p=parser")] + // public void Parse_Url_3x_Parser() + // { + // var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); + // commandLine.As(new OptionsParser()); + // } + // + // [Benchmark(Description = "parse [URL] -v=3.x -p=runtime")] + // public void Parse_Url_3x_Runtime() + // { + // var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); + // commandLine.As(); + // } + // + // [Benchmark(Description = "NuGet: CommandLineParser")] + // public void CommandLineParser() + // { + // Parser.Default.ParseArguments(GnuStyleArgs).WithParsed(options => { }); + // } + // + // [Benchmark(Description = "NuGet: System.CommandLine")] + // public void SystemCommandLine() + // { + // var fileOption = new System.CommandLine.Option( + // name: "--file", + // description: "The file to read and display on the console."); + // + // var rootCommand = new RootCommand("Benchmark for System.CommandLine"); + // rootCommand.AddOption(fileOption); + // rootCommand.SetHandler(file => { }, fileOption); + // + // rootCommand.Invoke(GnuStyleArgs); + // } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs new file mode 100644 index 00000000..10f72a28 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs @@ -0,0 +1,41 @@ +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse CMD Args")] +public class ParseCmdArgs +{ + [Benchmark(Description = "parse [CMD] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(CmdArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [CMD] -v=4.1 -p=powershell")] + public void Parse41_PowerShell() + { + var commandLine = CommandLine.Parse(CmdArgs, PowerShell); + commandLine.As(); + } + + [Benchmark(Description = "parse [CMD] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [CMD] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdArgs); + commandLine.As(); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs new file mode 100644 index 00000000..0ce31dbb --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs @@ -0,0 +1,41 @@ +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse DotNet Args")] +public class ParseDotNetArgs +{ + [Benchmark(Description = "parse [NET] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(DotNetArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [NET] -v=4.1 -p=dotnet")] + public void Parse41_PowerShell() + { + var commandLine = CommandLine.Parse(DotNetArgs, DotNet); + commandLine.As(); + } + + [Benchmark(Description = "parse [NET] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [NET] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetArgs); + commandLine.As(); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs new file mode 100644 index 00000000..43d40fd1 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.CommandLine; +using BenchmarkDotNet.Attributes; +using CommandLine; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse GNU Args")] +public class ParseGnuArgs +{ + [Benchmark(Description = "parse [GNU] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(GnuArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [GNU] -v=4.1 -p=gnu")] + public void Parse41_PowerShell() + { + var commandLine = CommandLine.Parse(GnuArgs, Gnu); + commandLine.As(); + } + + [Benchmark(Description = "parse [GNU] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [GNU] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuArgs); + commandLine.As(); + } + + [Benchmark(Description = "NuGet: CommandLineParser")] + public void CommandLineParser() + { + Parser.Default.ParseArguments(GnuArgs).WithParsed(options => { }); + } + + [Benchmark(Description = "NuGet: System.CommandLine")] + public void SystemCommandLine() + { + var debug = new Option("--debug"); + var count = new Option("--count"); + var testName = new Option("--test-name"); + var testCategory = new Option("--test-category"); + var detailLevel = new Option("--detail-level"); + var testItems = new Argument>(); + + var rootCommand = new RootCommand("Benchmark for System.CommandLine"); + rootCommand.AddOption(debug); + rootCommand.AddOption(count); + rootCommand.AddOption(testName); + rootCommand.AddOption(testCategory); + rootCommand.AddOption(detailLevel); + rootCommand.AddArgument(testItems); + rootCommand.SetHandler(file => { }); + + rootCommand.Invoke(GnuArgs); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs new file mode 100644 index 00000000..17e0eb7e --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs @@ -0,0 +1,34 @@ +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse MIX Args")] +public class ParseMixArgs +{ + [Benchmark(Description = "parse [MIX] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(MixArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [MIX] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(MixArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [MIX] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(MixArgs); + commandLine.As(); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs new file mode 100644 index 00000000..0fa4009d --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs @@ -0,0 +1,41 @@ +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse No Args")] +public class ParseNoArgs +{ + [Benchmark(Description = "parse [] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(NoArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [] -v=4.1 -p=dotnet")] + public void Parse41_PowerShell() + { + var commandLine = CommandLine.Parse(NoArgs, DotNet); + commandLine.As(); + } + + [Benchmark(Description = "parse [] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); + commandLine.As(); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs new file mode 100644 index 00000000..8f451556 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs @@ -0,0 +1,41 @@ +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Performance.Fakes; +using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +using static DotNetCampus.Cli.CommandLineParsingOptions; + +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +namespace DotNetCampus.Cli.Performance.ParseArgs; + +[MemoryDiagnoser] +[BenchmarkCategory("Parse PowerShell Args")] +public class ParsePowerShellArgs +{ + [Benchmark(Description = "parse [PS1] -v=4.1 -p=flexible")] + public void Parse41_Flexible() + { + var commandLine = CommandLine.Parse(PowerShellArgs, Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [PS1] -v=4.1 -p=powershell")] + public void Parse41_PowerShell() + { + var commandLine = CommandLine.Parse(PowerShellArgs, PowerShell); + commandLine.As(); + } + + [Benchmark(Description = "parse [PS1] -v=3.x -p=parser")] + public void Parse3x_Parser() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellArgs); + commandLine.As(new BenchmarkOption3Parser()); + } + + [Benchmark(Description = "parse [PS1] -v=3.x -p=runtime")] + public void Parse3x_Runtime() + { + var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellArgs); + commandLine.As(); + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Program.cs b/tests/DotNetCampus.CommandLine.Performance/Program.cs index a4ab5d2e..8d51fb16 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Program.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Program.cs @@ -1,4 +1,7 @@ -using System.Reflection; +using System; +using System.Linq; +using System.Reflection; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; namespace DotNetCampus.Cli.Performance; @@ -7,6 +10,24 @@ class Program { static void Main(string[] args) { + if (args.Contains("--debug")) + { + DebugAll(); + return; + } + BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); } + + private static void DebugAll() + { + var methods = typeof(Program).Assembly.GetTypes() + .Where(x => x.IsDefined(typeof(BenchmarkCategoryAttribute))) + .Select(x => (Instance: Activator.CreateInstance(x), Methods: x.GetMethods().Where(m => m.IsDefined(typeof(BenchmarkAttribute))))) + .SelectMany(x => x.Methods.Select(m => (x.Instance, m))); + foreach (var (instance, method) in methods) + { + method.Invoke(instance, []); + } + } } From 1abc6b9e41b30fb099f9a629e98c80b395404f34 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 10:36:44 +0800 Subject: [PATCH 042/193] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20ConsoleAppFramwork?= =?UTF-8?q?=20=E5=BA=93=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 1 + ...otNetCampus.CommandLine.Performance.csproj | 4 +++ .../BenchmarkOptionsConsoleAppFramework.cs | 26 +++++++++++++++++++ .../Fakes/CommandLineArguments.cs | 9 +++++++ .../ParseArgs/ParseGnuArgs.cs | 9 +++++++ 5 files changed, 49 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7800d8c0..1d7e50ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ + diff --git a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj index 9058d43a..fb2d062b 100644 --- a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj +++ b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs new file mode 100644 index 00000000..278fd6de --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using ConsoleAppFramework; + +namespace DotNetCampus.Cli.Performance.Fakes; + +public class BenchmarkOptionsConsoleAppFramework +{ + /// + /// 性能测试的命令行参数 + /// + /// 表示是否开启调试模式 + /// -c, 表示测试的次数 + /// -n, 表示测试的名称 + /// 表示测试的类别 + /// -d, 表示测试的详细等级 + /// 要测试的项目,可以是多个 + [Command("")] + [MethodImpl(MethodImplOptions.NoInlining)] + public void Root( + [Argument] IReadOnlyList testItems, + bool debug, int testCount, string? testName, + DetailLevel detailLevel, IReadOnlyList? testCategories = null) + { + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs index 8932ae45..e1f5562e 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs @@ -48,6 +48,15 @@ internal static class CommandLineArguments "--debug", ]; + public static readonly string[] GnuForConsoleAppFrameworkArgs = + [ + "DotNetCampus.CommandLine.Performance.dll,DotNetCampus.CommandLine.Sample.dll,DotNetCampus.CommandLine.Test.dll", + "-c", "20", + "--test-name", "BenchmarkTest", + "--detail-level", "High", + "--debug", + ]; + public static readonly string[] MixArgs = [ "DotNetCampus.CommandLine.Performance.dll", diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index 43d40fd1..3dc50bba 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -2,6 +2,7 @@ using System.CommandLine; using BenchmarkDotNet.Attributes; using CommandLine; +using ConsoleAppFramework; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; using static DotNetCampus.Cli.CommandLineParsingOptions; @@ -42,6 +43,14 @@ public void Parse3x_Runtime() commandLine.As(); } + [Benchmark(Description = "NuGet: ConsoleAppFramework")] + public void ConsoleAppFramework() + { + var app = ConsoleApp.Create(); + app.Add(); + app.Run(GnuForConsoleAppFrameworkArgs); + } + [Benchmark(Description = "NuGet: CommandLineParser")] public void CommandLineParser() { From 8d3355e9bd16226cafe05cf6b7875e315d2709c0 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 11:45:00 +0800 Subject: [PATCH 043/193] =?UTF-8?q?=E8=B0=83=E6=95=B4=20AOT=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs | 6 +++++- .../Compiler/CommandLineAttribute.cs | 4 +++- src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs | 3 +++ .../Compiler/RawArgumentsAttribute.cs | 3 +++ src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs | 3 +++ .../DotNetCampus.CommandLine.csproj | 8 ++++---- tests/DotNetCampus.CommandLine.Performance/Program.cs | 5 +++++ 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs index 0bf8cfbe..25a3a6f4 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs @@ -1,4 +1,6 @@ -namespace DotNetCampus.Cli.Compiler; +using System.Diagnostics; + +namespace DotNetCampus.Cli.Compiler; /// /// 将一个类绑定一个命令行命令。使用空格(` `)分隔多级子命令。 @@ -23,6 +25,7 @@ /// 多个 kebab-case 风格的词组以空格(` `)分隔,表示一个子命令或多级子命令。当启动程序传入多个命令且逐一匹配时,会匹配此类型。例如 `dotnet sln add`。 /// /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public sealed class CommandAttribute(string? names = null) : CommandLineAttribute { @@ -36,6 +39,7 @@ public sealed class CommandAttribute(string? names = null) : CommandLineAttribut /// 将一个类绑定一个命令行命令。使用空格(` `)分隔多级子命令。 /// /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] [Obsolete("因为子命令(MainCommand/SubCommand)具有更主流和广泛的认知,所以我们采用新名字 CommandAttribute 来替代 VerbAttribute。")] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public sealed class VerbAttribute(string? name) : CommandLineAttribute diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandLineAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/CommandLineAttribute.cs index 14b888d5..e70e27e4 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandLineAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandLineAttribute.cs @@ -1,10 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace DotNetCampus.Cli.Compiler; /// /// 为命令行参数与类型属性的关联提供特性基类。 /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] public abstract class CommandLineAttribute : Attribute { /// diff --git a/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs index 885b88d6..b6fbfe9f 100644 --- a/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace DotNetCampus.Cli.Compiler; /// @@ -35,6 +37,7 @@ namespace DotNetCampus.Cli.Compiler; /// do --property-name:key1=value1;key2=value2 /// /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class OptionAttribute : CommandLineAttribute { diff --git a/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs index 8b0b51a0..c816369a 100644 --- a/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs @@ -1,8 +1,11 @@ +using System.Diagnostics; + namespace DotNetCampus.Cli.Compiler; /// /// 标记在一个 string[] 或 IReadOnlyList<string> 类型的属性上,表示此属性将接收保留的原始命令行参数。 /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class RawArgumentsAttribute : CommandLineAttribute { diff --git a/src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs index 76c9d14f..25991652 100644 --- a/src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace DotNetCampus.Cli.Compiler; /// @@ -28,6 +30,7 @@ namespace DotNetCampus.Cli.Compiler; /// ImmutableDictionary<string, string> /// /// +[Conditional("FOR_SOURCE_GENERATION_ONLY")] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class ValueAttribute : CommandLineAttribute { diff --git a/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj b/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj index bb0eae71..91c1b5c9 100644 --- a/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj +++ b/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj @@ -16,15 +16,15 @@ snupkg true true - true - false + false + true true - + $(NoWarn);CS8767 - + diff --git a/tests/DotNetCampus.CommandLine.Performance/Program.cs b/tests/DotNetCampus.CommandLine.Performance/Program.cs index 8d51fb16..b90d6c20 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Program.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Program.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using BenchmarkDotNet.Attributes; @@ -10,15 +12,18 @@ class Program { static void Main(string[] args) { +#if DEBUG if (args.Contains("--debug")) { DebugAll(); return; } +#endif BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); } + [Conditional("DEBUG")] private static void DebugAll() { var methods = typeof(Program).Assembly.GetTypes() From f1aaaa77784500d9330c298b05cc2baa9e552567 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 12:27:03 +0800 Subject: [PATCH 044/193] =?UTF-8?q?=E5=9C=A8=20AOT=20=E4=B8=8B=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E6=94=AF=E6=8C=81=20AOT=20=E7=9A=84=E5=AF=B9?= =?UTF-8?q?=E7=85=A7=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...otNetCampus.CommandLine.Performance.csproj | 10 +- .../Others.cs | 186 +++++++++--------- .../ParseArgs/ParseGnuArgs.cs | 14 +- 3 files changed, 114 insertions(+), 96 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj index fb2d062b..98a4e8ff 100644 --- a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj +++ b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj @@ -5,6 +5,14 @@ net8.0 DotNetCampus.Cli.Performance + + + + false + true + $(DefineConstants);IS_USING_AOT + $(DefineConstants);IS_NOT_USING_AOT + @@ -12,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/DotNetCampus.CommandLine.Performance/Others.cs b/tests/DotNetCampus.CommandLine.Performance/Others.cs index d9dd8a99..07c65001 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Others.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Others.cs @@ -1,93 +1,93 @@ -using System.CommandLine; -using System.IO; -using BenchmarkDotNet.Attributes; -using dotnetCampus.Cli; -using Microsoft.Extensions.Options; -using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; - -namespace DotNetCampus.Cli.Performance; - -// [MemoryDiagnoser] -// [BenchmarkCategory("Parse No Args")] -public class Others -{ - // [Benchmark(Description = "handle [Edit,Print] --flexible")] - // public void Handle_Verbs_Flexible() - // { - // CommandLine.Parse(EditVerbArgs) - // .AddHandler(options => 0) - // .AddHandler(options => 0) - // .Run(); - // } - // - // [Benchmark(Description = "handle [Edit,Print] --dotnet")] - // public void Handle_Verbs_DotNet() - // { - // CommandLine.Parse(EditVerbArgs) - // .AddHandler(options => 0) - // .AddHandler(options => 0) - // .Run(); - // } - // - // [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] - // public void Handle_Verbs_Parser() - // { - // var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); - // commandLine - // .AddHandler(options => 0, new SelfWrittenEditOptionsParser()) - // .AddHandler(options => 0, new SelfWrittenPrintOptionsParser()) - // .Run(); - // } - // - // [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=runtime")] - // public void Handle_Verbs_Runtime() - // { - // var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); - // commandLine - // .AddHandler(options => 0) - // .AddHandler(options => 0) - // .Run(); - // } - // - // [Benchmark(Description = "parse [URL]")] - // public void Parse_Url() - // { - // var commandLine = CommandLine.Parse(UrlArgs, new CommandLineParsingOptions { SchemeNames = ["walterlv"] }); - // commandLine.As(); - // } - // - // [Benchmark(Description = "parse [URL] -v=3.x -p=parser")] - // public void Parse_Url_3x_Parser() - // { - // var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); - // commandLine.As(new OptionsParser()); - // } - // - // [Benchmark(Description = "parse [URL] -v=3.x -p=runtime")] - // public void Parse_Url_3x_Runtime() - // { - // var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); - // commandLine.As(); - // } - // - // [Benchmark(Description = "NuGet: CommandLineParser")] - // public void CommandLineParser() - // { - // Parser.Default.ParseArguments(GnuStyleArgs).WithParsed(options => { }); - // } - // - // [Benchmark(Description = "NuGet: System.CommandLine")] - // public void SystemCommandLine() - // { - // var fileOption = new System.CommandLine.Option( - // name: "--file", - // description: "The file to read and display on the console."); - // - // var rootCommand = new RootCommand("Benchmark for System.CommandLine"); - // rootCommand.AddOption(fileOption); - // rootCommand.SetHandler(file => { }, fileOption); - // - // rootCommand.Invoke(GnuStyleArgs); - // } -} +// using System.CommandLine; +// using System.IO; +// using BenchmarkDotNet.Attributes; +// using dotnetCampus.Cli; +// using Microsoft.Extensions.Options; +// using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; +// using static DotNetCampus.Cli.CommandLineParsingOptions; +// +// namespace DotNetCampus.Cli.Performance; +// +// // [MemoryDiagnoser] +// // [BenchmarkCategory("Parse No Args")] +// public class Others +// { +// [Benchmark(Description = "handle [Edit,Print] --flexible")] +// public void Handle_Verbs_Flexible() +// { +// CommandLine.Parse(EditVerbArgs) +// .AddHandler(options => 0) +// .AddHandler(options => 0) +// .Run(); +// } +// +// [Benchmark(Description = "handle [Edit,Print] --dotnet")] +// public void Handle_Verbs_DotNet() +// { +// CommandLine.Parse(EditVerbArgs) +// .AddHandler(options => 0) +// .AddHandler(options => 0) +// .Run(); +// } +// +// [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] +// public void Handle_Verbs_Parser() +// { +// var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); +// commandLine +// .AddHandler(options => 0, new SelfWrittenEditOptionsParser()) +// .AddHandler(options => 0, new SelfWrittenPrintOptionsParser()) +// .Run(); +// } +// +// [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=runtime")] +// public void Handle_Verbs_Runtime() +// { +// var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); +// commandLine +// .AddHandler(options => 0) +// .AddHandler(options => 0) +// .Run(); +// } +// +// [Benchmark(Description = "parse [URL]")] +// public void Parse_Url() +// { +// var commandLine = CommandLine.Parse(UrlArgs, new CommandLineParsingOptions { SchemeNames = ["walterlv"] }); +// commandLine.As(); +// } +// +// [Benchmark(Description = "parse [URL] -v=3.x -p=parser")] +// public void Parse_Url_3x_Parser() +// { +// var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); +// commandLine.As(new OptionsParser()); +// } +// +// [Benchmark(Description = "parse [URL] -v=3.x -p=runtime")] +// public void Parse_Url_3x_Runtime() +// { +// var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); +// commandLine.As(); +// } +// +// [Benchmark(Description = "NuGet: CommandLineParser")] +// public void CommandLineParser() +// { +// Parser.Default.ParseArguments(GnuStyleArgs).WithParsed(options => { }); +// } +// +// [Benchmark(Description = "NuGet: System.CommandLine")] +// public void SystemCommandLine() +// { +// var fileOption = new System.CommandLine.Option( +// name: "--file", +// description: "The file to read and display on the console."); +// +// var rootCommand = new RootCommand("Benchmark for System.CommandLine"); +// rootCommand.AddOption(fileOption); +// rootCommand.SetHandler(file => { }, fileOption); +// +// rootCommand.Invoke(GnuStyleArgs); +// } +// } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index 3dc50bba..b7ae8830 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -1,16 +1,21 @@ using System.Collections.Generic; -using System.CommandLine; using BenchmarkDotNet.Attributes; -using CommandLine; +using BenchmarkDotNet.Jobs; using ConsoleAppFramework; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; using static DotNetCampus.Cli.CommandLineParsingOptions; +#if IS_NOT_USING_AOT +using System.CommandLine; +using CommandLine; +#endif + // ReSharper disable ReturnValueOfPureMethodIsNotUsed namespace DotNetCampus.Cli.Performance.ParseArgs; +[SimpleJob(RuntimeMoniker.NativeAot10_0)] [MemoryDiagnoser] [BenchmarkCategory("Parse GNU Args")] public class ParseGnuArgs @@ -51,6 +56,8 @@ public void ConsoleAppFramework() app.Run(GnuForConsoleAppFrameworkArgs); } +#if IS_NOT_USING_AOT + [Benchmark(Description = "NuGet: CommandLineParser")] public void CommandLineParser() { @@ -78,4 +85,7 @@ public void SystemCommandLine() rootCommand.Invoke(GnuArgs); } + +#endif + } From 8607f3d7e632932d80c0a087714e6b69936534c1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 12:34:23 +0800 Subject: [PATCH 045/193] =?UTF-8?q?=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9Ahttps://github.com/dotnet/runtime/issues/119353?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParseArgs/ParseGnuArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index b7ae8830..3db99db6 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -15,7 +15,7 @@ namespace DotNetCampus.Cli.Performance.ParseArgs; -[SimpleJob(RuntimeMoniker.NativeAot10_0)] +[SimpleJob(RuntimeMoniker.NativeAot90)] [MemoryDiagnoser] [BenchmarkCategory("Parse GNU Args")] public class ParseGnuArgs From 1219d4a7de6125126db78243d649e66a1aa7b97f Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 13:21:30 +0800 Subject: [PATCH 046/193] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=AA=E4=BC=A0?= =?UTF-8?q?=E5=8F=82=E6=97=B6=E5=80=99=E7=9A=84=E6=80=A7=E8=83=BD=EF=BC=88?= =?UTF-8?q?=E7=9F=AD=E8=B7=AF=E8=A7=A3=E6=9E=90=E5=99=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 126 +++++++++++++++--- .../Models/GeneratingModelExtensions.cs | 32 +++++ 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 3f4a89e0..113ba7df 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -35,8 +35,8 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod $"public static {model.CommandObjectType.ToUsingString()} CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine)", m => m .AddRawStatements($"return new {model.Namespace}.{model.GetBuilderTypeName()}(commandLine).Build();")) - .AddRawMembers(model.OptionProperties.Select(GenerateOptionPropertyCode)) - .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateOptionPropertyCode)) + .AddRawMembers(model.OptionProperties.Select(GenerateArgumentPropertyCode)) + .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateArgumentPropertyCode)) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", @@ -63,25 +63,37 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", m => m .AddRawStatements(GenerateBuildCoreCode(model))) + .AddMethodDeclaration( + $"private {model.CommandObjectType.ToUsingString()} BuildDefault()", + m => m + .AddRawStatements(GenerateBuildDefaultCode(model))) .AddRawMembers(model.EnumerateEnumPropertyTypes().Select(GenerateEnumDeclarationCode)) ); return builder.ToString(); } - private string GenerateOptionPropertyCode(PropertyGeneratingModel model) => model.Type.AsCommandValueKind() switch + private string GenerateArgumentPropertyCode(PropertyGeneratingModel model) => + $"private {GetArgumentPropertyTypeName(model)} {model.PropertyName} = new();"; + + private string GetArgumentPropertyTypeName(PropertyGeneratingModel model) => model.Type.AsCommandValueKind() switch { - CommandValueKind.Boolean => $"private global::DotNetCampus.Cli.Compiler.BooleanArgument {model.PropertyName} = new();", - CommandValueKind.Number => $"private global::DotNetCampus.Cli.Compiler.NumberArgument {model.PropertyName} = new();", - CommandValueKind.Enum => $"private {model.Type.GetGeneratedEnumArgumentTypeName()} {model.PropertyName} = new();", - CommandValueKind.String => $"private global::DotNetCampus.Cli.Compiler.StringArgument {model.PropertyName} = new();", - CommandValueKind.List => $"private global::DotNetCampus.Cli.Compiler.StringListArgument {model.PropertyName} = new();", - CommandValueKind.Dictionary => $"private global::DotNetCampus.Cli.Compiler.StringDictionaryArgument {model.PropertyName} = new();", + CommandValueKind.Boolean => "global::DotNetCampus.Cli.Compiler.BooleanArgument", + CommandValueKind.Number => "global::DotNetCampus.Cli.Compiler.NumberArgument", + CommandValueKind.Enum => model.Type.GetGeneratedEnumArgumentTypeName(), + CommandValueKind.String => "global::DotNetCampus.Cli.Compiler.StringArgument", + CommandValueKind.List => "global::DotNetCampus.Cli.Compiler.StringListArgument", + CommandValueKind.Dictionary => "global::DotNetCampus.Cli.Compiler.StringDictionaryArgument", _ => $"// 不支持解析类型为 {model.Type.ToDisplayString()} 的属性 {model.PropertyName}。", }; private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $$""" public {{model.CommandObjectType.ToUsingString()}} Build() { + if (commandLine.RawArguments.Count is 0) + { + return BuildDefault(); + } + var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(commandLine, "{{model.CommandObjectType.Name}}", {{model.GetCommandLevel()}}) { MatchLongOption = MatchLongOption, @@ -238,21 +250,21 @@ private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) {{( initRawArgumentsProperties.Count is 0 ? " // There is no [RawArguments] property to be initialized." - : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentProperty)) + : string.Join("\n", initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false))) )}} // 2. [Option] {{( initOptionProperties.Count is 0 ? " // There is no [Option] property to be initialized." - : string.Join("\n", initOptionProperties.Select(GenerateInitProperty)) + : string.Join("\n", initOptionProperties.Select(x => GenerateInitProperty(x, false))) )}} // 3. [Value] {{( initPositionalArgumentProperties.Count is 0 ? " // There is no [Value] property to be initialized." - : string.Join("\n", initPositionalArgumentProperties.Select(GenerateInitProperty)) + : string.Join("\n", initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, false))) )}} }; @@ -260,7 +272,7 @@ initPositionalArgumentProperties.Count is 0 {{( setRawArgumentsProperties.Count is 0 ? "// There is no [RawArguments] property to be assigned." - : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentProperty)) + : string.Join("\n", setRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false))) )}} // 2. [Option] @@ -281,7 +293,49 @@ setPositionalArgumentProperties.Count is 0 """; } - private string GenerateInitProperty(PropertyGeneratingModel model) + private string GenerateBuildDefaultCode(CommandObjectGeneratingModel model) + { + var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); + var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); + var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequiredOrInit).ToList(); + if (initOptionProperties.Any(x => x.IsRequired) + || initPositionalArgumentProperties.Any(x => x.IsRequired)) + { + // 存在必须赋值的属性,不能生成默认值创建代码。 + return """ + throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException("The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", ""); + """; + } + + return $$""" + var result = new {{model.CommandObjectType.ToUsingString()}} + { + // 1. [RawArguments] + {{( + initRawArgumentsProperties.Count is 0 + ? " // There is no [RawArguments] property to be initialized." + : string.Join("\n", initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, true))) + )}} + + // 2. [Option] + {{( + initOptionProperties.Count is 0 + ? " // There is no [Option] property to be initialized." + : string.Join("\n", initOptionProperties.Select(x => GenerateInitProperty(x, true))) + )}} + + // 3. [Value] + {{( + initPositionalArgumentProperties.Count is 0 + ? " // There is no [Value] property to be initialized." + : string.Join("\n", initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, true))) + )}} + }; + return result; + """; + } + + private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefault) { // 对于不同的属性种类,如果命令行中没有赋值,则行为不同。 @@ -306,17 +360,44 @@ private string GenerateInitProperty(PropertyGeneratingModel model) (true, _, _, _) => model switch { OptionalArgumentPropertyGeneratingModel option => - $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", + $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required option '{option.GetOrdinalLongNames()[0]}'. Command line: {{commandLine}}\", \"{option.PropertyName}\")", PositionalArgumentPropertyGeneratingModel positionalArgument => - $" ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", + $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", _ => "", }, (_, true, true, _) => "", - (_, true, false, true) => " ?? null", - (_, true, false, false) => $" ?? default({model.Type.ToDisplayString()})", + (_, true, false, true) => "null", + (_, true, false, false) => $"default({model.Type.ToDisplayString()})", _ => "/* 非 init 属性,在下面单独赋值 */", }; - return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){fallback},"; + + if (!forDefault) + { + // 正常传入了命令行参数时的通用赋值。 + return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){(fallback is "" ? "" : $" ?? {fallback}")},"; + } + + if (fallback is not "") + { + // 未传命令行参数时,给非集合类型赋值。 + return $" {model.PropertyName} = {fallback},"; + } + + // 未传命令行参数时,给集合类型赋值为空集合。 + var supportCollectionExpression = model.Type.SupportCollectionExpression(true); + var supportCollectionExpressionLegacy = model.Type.SupportCollectionExpression(false); + return (supportCollectionExpression, supportCollectionExpressionLegacy) switch + { + (true, true) => $" {model.PropertyName} = [],", + (false, false) => $" {model.PropertyName} = new {GetArgumentPropertyTypeName(model)}().To{toTarget}(),", + _ => $""" + #if NET8_0_OR_GREATER + {model.PropertyName} = [], + #else + {model.PropertyName} = new {GetArgumentPropertyTypeName(model)}().To{toTarget}(), + #endif + """, + }; } private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex) @@ -337,8 +418,13 @@ private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex """; } - private string GenerateRawArgumentProperty(RawArgumentPropertyGeneratingModel model) + private string GenerateRawArgumentProperty(RawArgumentPropertyGeneratingModel model, bool forDefault) { + if (forDefault) + { + return $"{model.PropertyName} = [],"; + } + var assignment = $"{model.PropertyName} = (commandLine.CommandLineArguments as {model.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments]"; return model.IsRequiredOrInit ? $" {assignment}," diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs index eede7a9d..a3b0ec89 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs @@ -140,4 +140,36 @@ SpecialType.System_Double or _ => CommandValueKind.Unknown, }; } + + /// + /// 判断类型是否确定支持集合表达式(Collection Expression)。 + /// + /// 要检查的类型符号。 + /// 当前框架是否支持不可变集合类型(如 ImmutableListImmutableHashSet)。 + /// 如果类型确定支持集合表达式,则返回 ;否则返回 + public static bool SupportCollectionExpression(this ITypeSymbol typeSymbol, bool supportImmutableCollections) + { + if (typeSymbol.Kind is SymbolKind.ArrayType) + { + return true; + } + + var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); + if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) + { + // Nullable 类型 + var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return SupportCollectionExpression(genericType, supportImmutableCollections); + } + + return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch + { + "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" + or "IImmutableSet" or "IImmutableList" => true, + "List" or "Collection" or "HashSet" => true, + // 不可变集合在 .NET 8 及以上版本中支持集合表达式。 + "ImmutableArray" or "ImmutableHashSet" => supportImmutableCollections, + _ => false, + }; + } } From 8522e204a37e73c331dc2e362bec8b399d295618 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 13:54:51 +0800 Subject: [PATCH 047/193] =?UTF-8?q?=E5=90=91=E6=94=AF=E6=8C=81=E5=85=A8?= =?UTF-8?q?=E6=A0=88=E8=A7=A3=E6=9E=90=E9=9D=A0=E6=8B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 12 ++++---- .../Generators/ModelBuilderGenerator.cs | 12 ++++++-- .../ModelProviding/CommandModelProvider.cs | 3 ++ .../Models/CommandObjectGeneratingModel.cs | 2 ++ .../CommandRunnerBuilderExtensions.cs | 30 +++++++++---------- .../Compiler/CommandAttribute.cs | 7 ++++- .../Fakes/BenchmarkOptions4.cs | 22 ++++++++++++++ .../BenchmarkOptionsConsoleAppFramework.cs | 5 ++-- .../ParseArgs/ParseDotNetArgs.cs | 12 ++++++-- .../ParseArgs/ParseGnuArgs.cs | 5 ++-- 10 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index ae70dcf0..ef22dc18 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -155,11 +155,10 @@ private string GenerateCommandLineAsCode(ImmutableArray {{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} public static T CommandLine_As_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : {{model.CommandObjectType.ToGlobalDisplayString()}} { // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; // 否则下面的 {{model.GetBuilderTypeName()}} 类型将不存在,导致编译不通过。 - return (T)global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance(commandLine); + return (T)(object)new global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}(commandLine).Build(); } """; } @@ -172,8 +171,9 @@ private string GenerateCommandBuilderAddHandlerCode(ImmutableArray 方法的拦截器。拦截以提高性能。 /// {{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} - public static global::DotNetCampus.Cli.IAsyncCommandRunnerBuilder CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}(this global::DotNetCampus.Cli.ICoreCommandRunnerBuilder builder) - where T : {{model.CommandObjectType.ToGlobalDisplayString()}}, global::DotNetCampus.Cli.ICommandHandler + public static global::DotNetCampus.Cli.IAsyncCommandRunnerBuilder CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}( + this global::DotNetCampus.Cli.ICoreCommandRunnerBuilder builder) + where T : class, global::DotNetCampus.Cli.ICommandHandler { // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; // 否则下面的 {{model.GetBuilderTypeName()}} 类型将不存在,导致编译不通过。 @@ -191,8 +191,8 @@ private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray 方法的拦截器。拦截以提高性能。 /// {{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} - public static global::DotNetCampus.Cli.{{returnName}} CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}(this global::DotNetCampus.Cli.{{parameterThisName}} builder, - global::{{parameterTypeFullName}} handler) + public static global::DotNetCampus.Cli.{{returnName}} CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}( + this global::DotNetCampus.Cli.{{parameterThisName}} builder, global::{{parameterTypeFullName}} handler) where T : class { // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 113ba7df..6a89acfc 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -25,11 +25,10 @@ private void Execute(SourceProductionContext context, CommandObjectGeneratingMod private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) { - var modifier = model.IsPublic ? "public" : "internal"; var builder = new SourceTextBuilder(model.Namespace) .Using("System") .Using("DotNetCampus.Cli.Compiler") - .AddTypeDeclaration($"{modifier} sealed class {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)", t => t + .AddTypeDeclaration(GenerateBuilderTypeDeclarationLine(model), t => t .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") .AddMethodDeclaration( $"public static {model.CommandObjectType.ToUsingString()} CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine)", @@ -72,6 +71,13 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod return builder.ToString(); } + private static string GenerateBuilderTypeDeclarationLine(CommandObjectGeneratingModel model) + { + var modifier = model.IsPublic ? "public" : "internal"; + var type = model.UseFullStackParser ? "partial struct" : "sealed class"; + return $"{modifier} {type} {model.GetBuilderTypeName()}(global::DotNetCampus.Cli.CommandLine commandLine)"; + } + private string GenerateArgumentPropertyCode(PropertyGeneratingModel model) => $"private {GetArgumentPropertyTypeName(model)} {model.PropertyName} = new();"; @@ -303,7 +309,7 @@ private string GenerateBuildDefaultCode(CommandObjectGeneratingModel model) { // 存在必须赋值的属性,不能生成默认值创建代码。 return """ - throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException("The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", ""); + throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!); """; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 1bd313bb..c4e882f0 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -71,6 +71,8 @@ public static IncrementalValuesProvider SelectComm var @namespace = typeSymbol.ContainingNamespace.ToDisplayString(); var commandNames = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString(); + var useFullStackParser = attribute?.NamedArguments + .FirstOrDefault(kv => kv.Key == "ExperimentalUseFullStackParser").Value.Value as bool? ?? false; var isPublic = typeSymbol.DeclaredAccessibility == Accessibility.Public; for (var i = 0; i < optionProperties.Count; i++) @@ -86,6 +88,7 @@ public static IncrementalValuesProvider SelectComm { Namespace = @namespace, CommandObjectType = typeSymbol, + UseFullStackParser = useFullStackParser, IsPublic = isPublic, CommandNames = commandNames, IsHandler = isHandler, diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs index ad81ce6a..e7632275 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -13,6 +13,8 @@ internal record CommandObjectGeneratingModel public required string? CommandNames { get; init; } + public required bool UseFullStackParser { get; init; } + public required bool IsHandler { get; init; } public required IReadOnlyList RawArgumentsProperties { get; init; } diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index 8e9ca453..b3fd2d39 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -16,21 +16,21 @@ public static class CommandRunnerBuilderExtensions /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder) - where T : notnull, ICommandHandler + where T : class, ICommandHandler { throw CommandLine.MethodShouldBeInspected(); } /// public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } @@ -38,21 +38,21 @@ public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerB /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } /// public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } @@ -65,7 +65,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler) - where T : notnull + where T : class { throw CommandLine.MethodShouldBeInspected(); } @@ -82,7 +82,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull, ICommandHandler + where T : class, ICommandHandler { return builder.GetOrCreateRunner() .AddHandlerCore(command, factory); @@ -93,7 +93,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Action handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousCommandHandler(cl, factory, handler)); @@ -104,7 +104,7 @@ public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, factory); } @@ -114,7 +114,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerB public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousInt32CommandHandler(cl, factory, handler)); @@ -125,7 +125,7 @@ public static ICommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return (IAsyncCommandRunnerBuilder)((ICoreCommandRunnerBuilder)builder).AddHandler(handler, command, factory); } @@ -135,7 +135,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerB public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousTaskCommandHandler(cl, factory, handler)); @@ -154,7 +154,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBuilder builder, Func> handler, NamingPolicyNameGroup command, CommandObjectFactory factory ) - where T : notnull + where T : class { return builder.GetOrCreateRunner() .AddHandlerCore(command, cl => new AnonymousTaskInt32CommandHandler(cl, factory, handler)); @@ -167,7 +167,7 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令处理器集合的类型。 /// 命令行执行器构造的链式调用。 public static IAsyncCommandRunnerBuilder AddHandlers(this ICoreCommandRunnerBuilder builder) - where T : notnull, ICommandHandlerCollection, new() + where T : class, ICommandHandlerCollection, new() { throw new NotImplementedException(); } diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs index 25a3a6f4..a243e469 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandAttribute.cs @@ -26,13 +26,18 @@ namespace DotNetCampus.Cli.Compiler; /// /// [Conditional("FOR_SOURCE_GENERATION_ONLY")] -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)] public sealed class CommandAttribute(string? names = null) : CommandLineAttribute { /// /// 获取命令行的命令,可以是单个词组的主命令(Main Command),也可以是多个词组的子命令或多级子命令(Sub Command)。 /// public string? Names { get; } = names; + + /// + /// 实验性功能:完全使用栈上解析器,避免任何非业务的堆内存分配。 + /// + public bool ExperimentalUseFullStackParser { get; set; } } /// diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs index 99c4b910..330fb973 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs @@ -3,6 +3,28 @@ namespace DotNetCampus.Cli.Performance.Fakes; +[Command("", ExperimentalUseFullStackParser = true)] +public readonly record struct FullStackBenchmarkOptions4() +{ + [Option("debug")] + public required bool IsDebugMode { get; init; } + + [Option('c', "count")] + public required int TestCount { get; init; } + + [Option('n', "test-name")] + public string? TestName { get; init; } + + [Option("test-category")] + public string? TestCategory { get; init; } + + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; init; } + + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} + public class BenchmarkOptions4 { [Option("debug")] diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs index 278fd6de..a41a44a1 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptionsConsoleAppFramework.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Runtime.CompilerServices; using ConsoleAppFramework; @@ -18,9 +17,9 @@ public class BenchmarkOptionsConsoleAppFramework [Command("")] [MethodImpl(MethodImplOptions.NoInlining)] public void Root( - [Argument] IReadOnlyList testItems, + [Argument] string[] testItems, bool debug, int testCount, string? testName, - DetailLevel detailLevel, IReadOnlyList? testCategories = null) + DetailLevel detailLevel, string[]? testCategories = null) { } } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs index 0ce31dbb..4e86b4a8 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Attributes; +using System; +using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; using static DotNetCampus.Cli.CommandLineParsingOptions; @@ -19,12 +20,19 @@ public void Parse41_Flexible() } [Benchmark(Description = "parse [NET] -v=4.1 -p=dotnet")] - public void Parse41_PowerShell() + public void Parse41_Dotnet() { var commandLine = CommandLine.Parse(DotNetArgs, DotNet); commandLine.As(); } + [Benchmark(Description = "parse [NET] -v=4.1 -p=dotnet (struct)")] + public void Parse41_Dotnet_Struct() + { + var commandLine = CommandLine.Parse(DotNetArgs, DotNet); + var o = commandLine.As(); + } + [Benchmark(Description = "parse [NET] -v=3.x -p=parser")] public void Parse3x_Parser() { diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index 3db99db6..9a732385 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using ConsoleAppFramework; using DotNetCampus.Cli.Performance.Fakes; @@ -7,6 +6,7 @@ using static DotNetCampus.Cli.CommandLineParsingOptions; #if IS_NOT_USING_AOT +using System.Collections.Generic; using System.CommandLine; using CommandLine; #endif @@ -15,6 +15,7 @@ namespace DotNetCampus.Cli.Performance.ParseArgs; +[SimpleJob(RuntimeMoniker.Net10_0)] [SimpleJob(RuntimeMoniker.NativeAot90)] [MemoryDiagnoser] [BenchmarkCategory("Parse GNU Args")] From c2e594a0b789e4b8fe956e77b3b10abebd8f4eab Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 14:08:52 +0800 Subject: [PATCH 048/193] =?UTF-8?q?=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E5=8F=AF=E5=A4=84=E7=90=86=E5=85=A8=E6=A0=88=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 7 ++++++- .../ModelProviding/InterceptorModelProvider.cs | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index ef22dc18..6269935c 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -158,7 +158,12 @@ private string GenerateCommandLineAsCode(ImmutableArray(ref instance)" + : "(T)(object)instance" + )}}; } """; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs index 49b86465..85dddd44 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs @@ -107,13 +107,19 @@ public static IncrementalValuesProvider SelectMethod var commandNames = commandAttribute?.ConstructorArguments.FirstOrDefault() is { Kind: TypedConstantKind.Primitive } commandArgument ? commandArgument.Value?.ToString() : null; + var useFullStackParser = commandAttribute?.NamedArguments + .FirstOrDefault(kv => kv.Key == "ExperimentalUseFullStackParser").Value.Value as bool? ?? false; // 获取调用代码所在的类和方法。 var methodDeclaration = node.FirstAncestorOrSelf(); var classDeclaration = methodDeclaration?.FirstAncestorOrSelf(); var invocationFileName = Path.GetFileName(node.SyntaxTree.FilePath); var invocationInfo = $"{classDeclaration?.Identifier.ToString()}.{methodDeclaration?.Identifier.ToString()} @{invocationFileName}"; - return new InterceptorGeneratingModel(interceptableLocation, symbol, commandNames, invocationInfo); + return new InterceptorGeneratingModel(interceptableLocation, symbol, invocationInfo) + { + CommandNames = commandNames, + UseFullStackParser = useFullStackParser, + }; }) .Where(model => model is not null) .Select((model, ct) => model!); @@ -123,10 +129,13 @@ public static IncrementalValuesProvider SelectMethod internal record InterceptorGeneratingModel( InterceptableLocation InterceptableLocation, INamedTypeSymbol CommandObjectType, - string? CommandNames, string InvocationInfo ) { + public required string? CommandNames { get; init; } + + public required bool UseFullStackParser { get; init; } + public string GetBuilderTypeName() => CommandObjectGeneratingModel.GetBuilderTypeName(CommandObjectType); public string? GetPascalCaseCommandNames() From c3c630a27d9a6f0919921d6ec554e2445723efd6 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 14:30:57 +0800 Subject: [PATCH 049/193] =?UTF-8?q?=E9=80=90=E6=AD=A5=E5=BC=80=E5=A7=8B?= =?UTF-8?q?=E6=A0=88=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 6a89acfc..93ff5581 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -1,6 +1,8 @@ +using System.Text; using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; using DotNetCampus.CommandLine.Generators.Models; +using DotNetCampus.CommandLine.IO; using Microsoft.CodeAnalysis; namespace DotNetCampus.CommandLine.Generators; @@ -21,6 +23,15 @@ private void Execute(SourceProductionContext context, CommandObjectGeneratingMod { var code = GenerateCommandObjectCreatorCode(model); context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.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 string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel model) @@ -483,4 +494,12 @@ 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(); } From 20c59402aca9875152b18d0295b8a71d92b88688 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 18:14:57 +0800 Subject: [PATCH 050/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20SourceTextBuilder?= =?UTF-8?q?=20=E7=94=9F=E6=88=90=E6=8B=A6=E6=88=AA=E5=99=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 152 ++++++++---------- 1 file changed, 71 insertions(+), 81 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index 6269935c..a267228e 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -1,6 +1,7 @@ #pragma warning disable RSEXPERIMENTAL002 using System.Collections.Immutable; using DotNetCampus.Cli.Utils; +using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; using DotNetCampus.CommandLine.Utils.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -105,106 +106,95 @@ private void CommandRunnerAddHandlerAction(SourceProductionContext context, return; } - var code = GenerateCode(modelGroups, x => GenerateCommandBuilderAddHandlerActionCode(x, parameterThisName, parameterTypeFullName, returnName)); + var code = GenerateCode(modelGroups, (t, x) => + GenerateCommandBuilderAddHandlerActionCode(t, x, parameterThisName, parameterTypeFullName, returnName)); context.AddSource($"CommandLine.Interceptors/{fileName}.g.cs", code); } private string GenerateCode(Dictionary> models, - Func, string> methodCreator) + Action> methodCreator) { - return $$""" -#nullable enable - -namespace DotNetCampus.Cli.Compiler -{ - file static class Interceptors - { -{{string.Join("\n\n", models.Select(x => methodCreator(x.Value)))}} + var builder = new SourceTextBuilder() + .AddNamespaceDeclaration("DotNetCampus.Cli.Compiler", n => n + .AddTypeDeclaration("file static class Interceptors", t => + { + foreach (var pair in models) + { + methodCreator(t, pair.Value); + } + })) + .AddNamespaceDeclaration("System.Runtime.CompilerServices", n => n + .AddTypeDeclaration("file sealed class InterceptsLocationAttribute : global::System.Attribute", t => t + .AddAttribute("""[global::System.Diagnostics.Conditional("FOR_SOURCE_GENERATION_ONLY")]""") + .AddAttribute("[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]") + .AddRawText(""" + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + """))); + return builder.ToString(); } -} - -namespace System.Runtime.CompilerServices -{ - [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : global::System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - _ = version; - _ = data; - } - } -} -"""; - } - - private string GenerateInterceptsLocationCode(InterceptorGeneratingModel model) - { - return $""" + private string GenerateInterceptsLocationCode(InterceptorGeneratingModel model) => $""" [global::System.Runtime.CompilerServices.InterceptsLocation({model.InterceptableLocation.Version}, /* {model.InvocationInfo} */ "{model.InterceptableLocation.Data}")] -"""; - } + """; - private string GenerateCommandLineAsCode(ImmutableArray models) + private void GenerateCommandLineAsCode(TypeDeclarationSourceTextBuilder builder, IReadOnlyList models) { var model = models[0]; - return $$""" - /// - /// 方法的拦截器。拦截以提高性能。 - /// -{{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} - public static T CommandLine_As_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}(this global::DotNetCampus.Cli.CommandLine commandLine) - { - // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; - // 否则下面的 {{model.GetBuilderTypeName()}} 类型将不存在,导致编译不通过。 - var instance = new global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}(commandLine).Build(); - return {{( - model.UseFullStackParser - ? $"global::System.Runtime.CompilerServices.Unsafe.As<{model.CommandObjectType.ToGlobalDisplayString()}, T>(ref instance)" - : "(T)(object)instance" - )}}; - } -"""; + builder.AddMethodDeclaration( + $"public static T CommandLine_As_{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}(this global::DotNetCampus.Cli.CommandLine commandLine)", + m => m + .WithSummaryComment($$""" 方法的拦截器。拦截以提高性能。""") + .AddAttributes(models.Select(GenerateInterceptsLocationCode)) + .AddTypeConstraints(model.UseFullStackParser ? "where T : struct" : "where T : notnull") + .AddRawStatements($""" + // 请确保 {model.CommandObjectType.Name} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; + // 否则下面的 {model.GetBuilderTypeName()} 类型将不存在,导致编译不通过。 + var instance = new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}(commandLine).Build(); + return {( + model.UseFullStackParser + ? $"global::System.Runtime.CompilerServices.Unsafe.BitCast<{model.CommandObjectType.ToGlobalDisplayString()}, T>(instance)" + : "(T)(object)instance" + )}; + """)); } - private string GenerateCommandBuilderAddHandlerCode(ImmutableArray models) + private void GenerateCommandBuilderAddHandlerCode(TypeDeclarationSourceTextBuilder builder, IReadOnlyList models) { var model = models[0]; - return $$""" - /// - /// 方法的拦截器。拦截以提高性能。 - /// -{{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} - public static global::DotNetCampus.Cli.IAsyncCommandRunnerBuilder CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}( - this global::DotNetCampus.Cli.ICoreCommandRunnerBuilder builder) - where T : class, global::DotNetCampus.Cli.ICommandHandler - { - // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; - // 否则下面的 {{model.GetBuilderTypeName()}} 类型将不存在,导致编译不通过。 - return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, {{model.ToNameGroup()}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); - } -"""; + builder.AddMethodDeclaration($""" + public static global::DotNetCampus.Cli.IAsyncCommandRunnerBuilder CommandBuilder_AddHandler_{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}(this global::DotNetCampus.Cli.ICoreCommandRunnerBuilder builder) + """, m => m + .WithSummaryComment( + $$""" 方法的拦截器。拦截以提高性能。""") + .AddAttributes(models.Select(GenerateInterceptsLocationCode)) + .AddTypeConstraints("where T : class, global::DotNetCampus.Cli.ICommandHandler") + .AddRawStatements($""" + // 请确保 {model.CommandObjectType.Name} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; + // 否则下面的 {model.GetBuilderTypeName()} 类型将不存在,导致编译不通过。 + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, {model.ToNameGroup()}, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + """)); } - private string GenerateCommandBuilderAddHandlerActionCode(ImmutableArray models, string parameterThisName, - string parameterTypeFullName, string returnName) + private void GenerateCommandBuilderAddHandlerActionCode(TypeDeclarationSourceTextBuilder builder, IReadOnlyList models, + string parameterThisName, string parameterTypeFullName, string returnName) { var model = models[0]; - return $$""" - /// - /// 方法的拦截器。拦截以提高性能。 - /// -{{string.Join("\n", models.Select(GenerateInterceptsLocationCode))}} - public static global::DotNetCampus.Cli.{{returnName}} CommandBuilder_AddHandler_{{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}}( - this global::DotNetCampus.Cli.{{parameterThisName}} builder, global::{{parameterTypeFullName}} handler) - where T : class - { - // 请确保 {{model.CommandObjectType.Name}} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; - // 否则下面的 {{model.GetBuilderTypeName()}} 类型将不存在,导致编译不通过。 - return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, {{model.ToNameGroup()}}, global::{{model.CommandObjectType.ContainingNamespace}}.{{model.GetBuilderTypeName()}}.CreateInstance); - } -"""; + builder.AddMethodDeclaration($""" + public static global::DotNetCampus.Cli.{returnName} CommandBuilder_AddHandler_{NamingHelper.MakePascalCase(model.CommandObjectType.ToDisplayString())}(this global::DotNetCampus.Cli.{parameterThisName} builder, global::{parameterTypeFullName} handler) + """, m => m + .WithSummaryComment( + $$""" 方法的拦截器。拦截以提高性能。""") + .AddAttributes(models.Select(GenerateInterceptsLocationCode)) + .AddTypeConstraints("where T : class") + .AddRawStatements($""" + // 请确保 {model.CommandObjectType.Name} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; + // 否则下面的 {model.GetBuilderTypeName()} 类型将不存在,导致编译不通过。 + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, {model.ToNameGroup()}, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + """)); } } From 512efda6cc053676b7203b7177292daaaf994730 Mon Sep 17 00:00:00 2001 From: walterlv Date: Wed, 17 Sep 2025 18:34:33 +0800 Subject: [PATCH 051/193] =?UTF-8?q?=E6=94=AF=E6=8C=81=20BitCast=20?= =?UTF-8?q?=E5=92=8C=20As=20=E6=A0=B9=E6=8D=AE=E7=9B=AE=E6=A0=87=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E8=87=AA=E5=8A=A8=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/InterceptorGenerator.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index a267228e..03333989 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -154,11 +154,17 @@ private void GenerateCommandLineAsCode(TypeDeclarationSourceTextBuilder builder, // 请确保 {model.CommandObjectType.Name} 类型中至少有一个属性标记了 [Option] 或 [Value] 特性; // 否则下面的 {model.GetBuilderTypeName()} 类型将不存在,导致编译不通过。 var instance = new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}(commandLine).Build(); - return {( + {( model.UseFullStackParser - ? $"global::System.Runtime.CompilerServices.Unsafe.BitCast<{model.CommandObjectType.ToGlobalDisplayString()}, T>(instance)" - : "(T)(object)instance" - )}; + ? $""" + #if NET8_0_OR_GREATER + return global::System.Runtime.CompilerServices.Unsafe.BitCast<{model.CommandObjectType.ToGlobalDisplayString()}, T>(instance); + #else + return global::System.Runtime.CompilerServices.Unsafe.As<{model.CommandObjectType.ToGlobalDisplayString()}, T>(ref instance); + #endif + """ + : "return (T)(object)instance;" + )} """)); } From 3bc692cbb699e052ad46e3dc4ec206b1c222d8e1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 00:29:09 +0800 Subject: [PATCH 052/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=9B=B4=E5=8A=A0?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E7=9A=84=E6=BA=90=E4=BB=A3=E7=A0=81=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=99=A8=E7=9A=84=E6=96=B9=E6=B3=95=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 93ff5581..64904811 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -2,7 +2,6 @@ using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; using DotNetCampus.CommandLine.Generators.Models; -using DotNetCampus.CommandLine.IO; using Microsoft.CodeAnalysis; namespace DotNetCampus.CommandLine.Generators; @@ -66,7 +65,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddMethodDeclaration( "private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value)", m => m - .BeginBracketScope("switch (propertyIndex)", l => l + .AddBracketScope("switch (propertyIndex)", l => l .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode)) .AddRawStatements(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode)))) .AddMethodDeclaration( From 954aaab4ec2c121f131a0d0415148986fbc1c2ab Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 08:33:57 +0800 Subject: [PATCH 053/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20builder=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=8C=B9=E9=85=8D=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 135 ++++++++++++------ 1 file changed, 92 insertions(+), 43 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 64904811..a9d34a54 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -49,14 +49,10 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", - m => m - .AddRawStatements(GenerateMatchLongOptionCode(model)) - .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;")) + m => GenerateMatchLongOptionCode(m, model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive)", - m => m - .AddRawStatements(GenerateMatchShortOptionCode(model)) - .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;")) + m => GenerateMatchShortOptionCode(m, model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex)", m => m @@ -122,56 +118,71 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ } """; - private string GenerateMatchLongOptionCode(CommandObjectGeneratingModel model) + private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return optionProperties.Count is 0 - ? "// 没有长名称选项,无需匹配。" - : $$""" - var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; - - // 先原样匹配一遍。 - if (namingPolicy.SupportsOrdinal()) + if (optionProperties.Count is 0) + { + builder.AddRawStatement("// 没有长名称选项,无需匹配。"); + } + else { - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetOrdinalLongNames())))}} + builder + .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") + .AddBracketScope("switch (longOption)", s => s + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionCaseCode(x, x.GetOrdinalLongNames())))) + .AddDefaultStringComparisonIfNeeded(optionProperties) + .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") + .AddBracketScope("if (namingPolicy.SupportsOrdinal())", s => s + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetOrdinalLongNames())))) + .AddRawStatement("// 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。") + .AddBracketScope("if (namingPolicy.SupportsPascalCase())", s => s + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames())))); } + return builder + .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); - // 再根据命名法匹配一遍(只匹配与上述名称不同的名称)。 - if (namingPolicy.SupportsPascalCase()) + static string GenerateLongOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetPascalCaseLongNames())))}} + return string.Join("\n", names.Select(name => $""" + case "{name}": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({model.PropertyName}), {model.PropertyIndex}, {model.Type.AsCommandValueKind().ToCommandValueTypeName()}); + """)); } - """; - static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) + static string GenerateLongOptionEqualsCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { - var comparison = model.CaseSensitive switch - { - true => "global::System.StringComparison.Ordinal", - false => "global::System.StringComparison.OrdinalIgnoreCase", - null => "defaultComparison", - }; return string.Join("\n", names.Select(name => $$""" - if (longOption.Equals("{{name}}".AsSpan(), {{comparison}})) + if (longOption.Equals("{{name}}".AsSpan(), {{model.GetStringComparisonExpression()}})) { return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({{model.PropertyName}}), {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } - """)); + """)); } } - private string GenerateMatchShortOptionCode(CommandObjectGeneratingModel model) + private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return optionProperties.Count is 0 - ? "// 没有短名称选项,无需匹配。" - : $$""" - var defaultComparison = defaultCaseSensitive ? global::System.StringComparison.Ordinal : global::System.StringComparison.OrdinalIgnoreCase; + if (optionProperties.Count is 0) + { + builder.AddRawStatement("// 没有短名称选项,无需匹配。"); + } + else + { + builder + .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") + .AddBracketScope("switch (shortOption)", s => s + .AddRawStatements(optionProperties.Select(x => GenerateOptionCaseCode(x, x.GetShortNames())))) + .AddDefaultStringComparisonIfNeeded(optionProperties) + .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") + .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetOrdinalLongNames()))); + } - {{string.Join("\n", optionProperties.Select(x => GenerateOptionMatchCode(x, x.GetShortNames())))}} - """; + return builder + .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); - static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) + static string GenerateOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) { if (names.Count == 0) { @@ -179,14 +190,22 @@ static string GenerateOptionMatchCode(OptionalArgumentPropertyGeneratingModel mo // 属性 {model.PropertyName} 没有短名称,无需匹配。 """; } - var comparison = model.CaseSensitive switch + return string.Join("\n", names.Select(name => $""" + case "{name}": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({model.PropertyName}), {model.PropertyIndex}, {model.Type.AsCommandValueKind().ToCommandValueTypeName()}); + """)); + } + + static string GenerateOptionEqualsCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) + { + if (names.Count == 0) { - true => "global::System.StringComparison.Ordinal", - false => "global::System.StringComparison.OrdinalIgnoreCase", - null => "defaultComparison", - }; + return $""" + // 属性 {model.PropertyName} 没有短名称,无需匹配。 + """; + } return string.Join("\n", names.Select(name => $$""" - if (shortOption.Equals("{{name}}".AsSpan(), {{comparison}})) + if (shortOption.Equals("{{name}}".AsSpan(), {{model.GetStringComparisonExpression()}})) { return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({{model.PropertyName}}), {{model.PropertyIndex}}, {{model.Type.AsCommandValueKind().ToCommandValueTypeName()}}); } @@ -383,7 +402,7 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau }, (_, true, true, _) => "", (_, true, false, true) => "null", - (_, true, false, false) => $"default({model.Type.ToDisplayString()})", + (_, true, false, false) => $"default({model.Type.ToDisplayString()})!", _ => "/* 非 init 属性,在下面单独赋值 */", }; @@ -502,3 +521,33 @@ private string GenerateEnumDeclarationCode(ITypeSymbol enumType) .Replace("public readonly ref struct CommandLineParser", $"partial struct {model.GetBuilderTypeName()}") .ToString(); } + +file static class Extensions +{ + public static string GetStringComparisonExpression(this OptionalArgumentPropertyGeneratingModel model) + { + return model.CaseSensitive switch + { + true => "global::System.StringComparison.Ordinal", + false => "global::System.StringComparison.OrdinalIgnoreCase", + null => "defaultComparison", + }; + } + + public static TBuilder AddDefaultStringComparisonIfNeeded(this TBuilder builder, + IReadOnlyList optionProperties) + where TBuilder : IAllowStatements + { + var needStringComparison = optionProperties.Any(x => x.CaseSensitive is null); + if (needStringComparison) + { + builder.AddRawStatement( + """ + var defaultComparison = defaultCaseSensitive + ? global::System.StringComparison.Ordinal + : global::System.StringComparison.OrdinalIgnoreCase; + """); + } + return builder; + } +} From f81cbd4e4f5e9790fcfa0268f43f06f4c9496f80 Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 08:48:20 +0800 Subject: [PATCH 054/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=B5=81=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index a9d34a54..5ca9a411 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -121,13 +121,10 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - if (optionProperties.Count is 0) - { - builder.AddRawStatement("// 没有长名称选项,无需匹配。"); - } - else - { - builder + return builder + .Condition(optionProperties.Count is 0, b => b + .AddRawStatement("// 没有长名称选项,无需匹配。")) + .Otherwise(b => b .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") .AddBracketScope("switch (longOption)", s => s .AddRawStatements(optionProperties.Select(x => GenerateLongOptionCaseCode(x, x.GetOrdinalLongNames())))) @@ -137,9 +134,8 @@ private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDec .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetOrdinalLongNames())))) .AddRawStatement("// 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。") .AddBracketScope("if (namingPolicy.SupportsPascalCase())", s => s - .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames())))); - } - return builder + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames()))))) + .EndCondition() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateLongOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) @@ -164,22 +160,17 @@ static string GenerateLongOptionEqualsCode(OptionalArgumentPropertyGeneratingMod private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - if (optionProperties.Count is 0) - { - builder.AddRawStatement("// 没有短名称选项,无需匹配。"); - } - else - { - builder + return builder + .Condition(optionProperties.Count is 0, b => b + .AddRawStatement("// 没有短名称选项,无需匹配。")) + .Otherwise(b => b .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") .AddBracketScope("switch (shortOption)", s => s .AddRawStatements(optionProperties.Select(x => GenerateOptionCaseCode(x, x.GetShortNames())))) .AddDefaultStringComparisonIfNeeded(optionProperties) .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") - .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetOrdinalLongNames()))); - } - - return builder + .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetShortNames())))) + .EndCondition() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) From 66b19553f17638bbfea90d4af77ac4631ba6397d Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 09:50:49 +0800 Subject: [PATCH 055/193] =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=A9=BA=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 5ca9a411..ea426460 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -128,14 +128,18 @@ private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDec .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") .AddBracketScope("switch (longOption)", s => s .AddRawStatements(optionProperties.Select(x => GenerateLongOptionCaseCode(x, x.GetOrdinalLongNames())))) + .AddLineSeparator() .AddDefaultStringComparisonIfNeeded(optionProperties) + .AddLineSeparator() .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") .AddBracketScope("if (namingPolicy.SupportsOrdinal())", s => s .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetOrdinalLongNames())))) + .AddLineSeparator() .AddRawStatement("// 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。") .AddBracketScope("if (namingPolicy.SupportsPascalCase())", s => s .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames()))))) .EndCondition() + .AddLineSeparator() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateLongOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) @@ -160,8 +164,9 @@ static string GenerateLongOptionEqualsCode(OptionalArgumentPropertyGeneratingMod private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; + var hasShortName = optionProperties.SelectMany(x => x.GetShortNames()).Any(); return builder - .Condition(optionProperties.Count is 0, b => b + .Condition(!hasShortName, b => b .AddRawStatement("// 没有短名称选项,无需匹配。")) .Otherwise(b => b .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") @@ -184,7 +189,7 @@ static string GenerateOptionCaseCode(OptionalArgumentPropertyGeneratingModel mod return string.Join("\n", names.Select(name => $""" case "{name}": return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof({model.PropertyName}), {model.PropertyIndex}, {model.Type.AsCommandValueKind().ToCommandValueTypeName()}); - """)); + """)); } static string GenerateOptionEqualsCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) From 22c4b2af221b3ee0a68761db08fcd4830e1985ca Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 10:33:23 +0800 Subject: [PATCH 056/193] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E4=B9=9F=E7=94=A8=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 186 ++++++++++-------- 1 file changed, 100 insertions(+), 86 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index ea426460..925f96cd 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -66,12 +66,10 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddRawStatements(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode)))) .AddMethodDeclaration( $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", - m => m - .AddRawStatements(GenerateBuildCoreCode(model))) + m => GenerateBuildCoreCode(m, model)) .AddMethodDeclaration( $"private {model.CommandObjectType.ToUsingString()} BuildDefault()", - m => m - .AddRawStatements(GenerateBuildDefaultCode(model))) + m => GenerateBuildDefaultCode(m, model)) .AddRawMembers(model.EnumerateEnumPropertyTypes().Select(GenerateEnumDeclarationCode)) ); return builder.ToString(); @@ -172,10 +170,13 @@ private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDe .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") .AddBracketScope("switch (shortOption)", s => s .AddRawStatements(optionProperties.Select(x => GenerateOptionCaseCode(x, x.GetShortNames())))) + .AddLineSeparator() .AddDefaultStringComparisonIfNeeded(optionProperties) + .AddLineSeparator() .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetShortNames())))) .EndCondition() + .AddLineSeparator() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) @@ -266,7 +267,7 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) """; } - private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) + private MethodDeclarationSourceTextBuilder GenerateBuildCoreCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); @@ -274,96 +275,109 @@ private string GenerateBuildCoreCode(CommandObjectGeneratingModel model) var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequiredOrInit).ToList(); var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequiredOrInit).ToList(); var setPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => !x.IsRequiredOrInit).ToList(); - return $$""" - var result = new {{model.CommandObjectType.ToUsingString()}} - { - // 1. [RawArguments] - {{( - initRawArgumentsProperties.Count is 0 - ? " // There is no [RawArguments] property to be initialized." - : string.Join("\n", initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false))) - )}} - - // 2. [Option] - {{( - initOptionProperties.Count is 0 - ? " // There is no [Option] property to be initialized." - : string.Join("\n", initOptionProperties.Select(x => GenerateInitProperty(x, false))) - )}} - - // 3. [Value] - {{( - initPositionalArgumentProperties.Count is 0 - ? " // There is no [Value] property to be initialized." - : string.Join("\n", initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, false))) - )}} - }; - // 1. [RawArguments] - {{( - setRawArgumentsProperties.Count is 0 - ? "// There is no [RawArguments] property to be assigned." - : string.Join("\n", setRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false))) - )}} - - // 2. [Option] - {{( - setOptionProperties.Count is 0 - ? "// There is no [RawArguments] property to be assigned." - : string.Join("\n", setOptionProperties.Select(GenerateSetProperty)) - )}} - - // 3. [Value] - {{( - setPositionalArgumentProperties.Count is 0 - ? "// There is no [RawArguments] property to be assigned." - : string.Join("\n", setPositionalArgumentProperties.Select(GenerateSetProperty)) - )}} - - return result; - """; + return builder + .AddBracketScope($"var result = new {model.CommandObjectType.ToUsingString()}", "{", "};", c => c + + // 1. [RawArguments] + .Condition(initRawArgumentsProperties.Count is 0, b => b + .AddRawStatement("// 1. There is no [RawArguments] property to be initialized.")) + .Otherwise(b => b + .AddRawStatement("// 1. [RawArguments]") + .AddRawStatements(initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false)))) + .EndCondition() + + // 2. [Option] + .AddLineSeparator() + .Condition(initOptionProperties.Count is 0, b => b + .AddRawStatement("// 2. There is no [Option] property to be initialized.")) + .Otherwise(b => b + .AddRawStatement("// 2. [Option]") + .AddRawStatements(initOptionProperties.Select(x => GenerateInitProperty(x, false)))) + .EndCondition() + + // 3. [Value] + .AddLineSeparator() + .Condition(initPositionalArgumentProperties.Count is 0, b => b + .AddRawStatement("// 3. There is no [Value] property to be initialized.")) + .Otherwise(b => b + .AddRawStatement("// 3. [Value]") + .AddRawStatements(initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, false)))) + .EndCondition()) + + // 1. [RawArguments] + .AddLineSeparator() + .Condition(setRawArgumentsProperties.Count is 0, b => b + .AddRawStatement("// 1. There is no [RawArguments] property to be assigned.")) + .Otherwise(b => b + .AddRawStatement("// 1. [RawArguments]") + .AddRawStatements(setRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, false)))) + .EndCondition() + + // 2. [Option] + .AddLineSeparator() + .Condition(setOptionProperties.Count is 0, b => b + .AddRawStatement("// 2. There is no [Option] property to be assigned.")) + .Otherwise(b => b + .AddRawStatement("// 2. [Option]") + .AddRawStatements(setOptionProperties.Select(GenerateSetProperty))) + .EndCondition() + + // 3. [Value] + .AddLineSeparator() + .Condition(setPositionalArgumentProperties.Count is 0, b => b + .AddRawStatement("// 3. There is no [Value] property to be assigned.")) + .Otherwise(b => b + .AddRawStatement("// 3. [Value]") + .AddRawStatements(setPositionalArgumentProperties.Select(GenerateSetProperty))) + .EndCondition() + .AddLineSeparator() + + // 返回。 + .AddRawStatement("return result;"); } - private string GenerateBuildDefaultCode(CommandObjectGeneratingModel model) + private MethodDeclarationSourceTextBuilder GenerateBuildDefaultCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); var initPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => x.IsRequiredOrInit).ToList(); + if (initOptionProperties.Any(x => x.IsRequired) || initPositionalArgumentProperties.Any(x => x.IsRequired)) { // 存在必须赋值的属性,不能生成默认值创建代码。 - return """ - throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!); - """; + 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 $$""" - var result = new {{model.CommandObjectType.ToUsingString()}} - { - // 1. [RawArguments] - {{( - initRawArgumentsProperties.Count is 0 - ? " // There is no [RawArguments] property to be initialized." - : string.Join("\n", initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, true))) - )}} - - // 2. [Option] - {{( - initOptionProperties.Count is 0 - ? " // There is no [Option] property to be initialized." - : string.Join("\n", initOptionProperties.Select(x => GenerateInitProperty(x, true))) - )}} - - // 3. [Value] - {{( - initPositionalArgumentProperties.Count is 0 - ? " // There is no [Value] property to be initialized." - : string.Join("\n", initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, true))) - )}} - }; - return result; - """; + return builder + .AddBracketScope($"var result = new {model.CommandObjectType.ToUsingString()}", "{", "};", c => c + + // 1. [RawArguments] + .Condition(initRawArgumentsProperties.Count is 0, b => b.Ignore()) + .Otherwise(b => b + .AddRawStatements(initRawArgumentsProperties.Select(x => GenerateRawArgumentProperty(x, true)))) + .EndCondition() + + // 2. [Option] + .AddLineSeparator() + .Condition(initOptionProperties.Count is 0, b => b.Ignore()) + .Otherwise(b => b + .AddRawStatements(initOptionProperties.Select(x => GenerateInitProperty(x, true)))) + .EndCondition() + + // 3. [Value] + .AddLineSeparator() + .Condition(initPositionalArgumentProperties.Count is 0, b => b.Ignore()) + .Otherwise(b => b + .AddRawStatements(initPositionalArgumentProperties.Select(x => GenerateInitProperty(x, true)))) + .EndCondition()) + + // 返回。 + .AddRawStatement("return result;"); } private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefault) @@ -405,13 +419,13 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau if (!forDefault) { // 正常传入了命令行参数时的通用赋值。 - return $" {model.PropertyName} = {model.PropertyName}.To{toTarget}(){(fallback is "" ? "" : $" ?? {fallback}")},"; + return $"{model.PropertyName} = {model.PropertyName}.To{toTarget}(){(fallback is "" ? "" : $" ?? {fallback}")},"; } if (fallback is not "") { // 未传命令行参数时,给非集合类型赋值。 - return $" {model.PropertyName} = {fallback},"; + return $"{model.PropertyName} = {fallback},"; } // 未传命令行参数时,给集合类型赋值为空集合。 @@ -427,7 +441,7 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau #else {model.PropertyName} = new {GetArgumentPropertyTypeName(model)}().To{toTarget}(), #endif - """, + """, }; } @@ -458,7 +472,7 @@ private string GenerateRawArgumentProperty(RawArgumentPropertyGeneratingModel mo var assignment = $"{model.PropertyName} = (commandLine.CommandLineArguments as {model.Type.ToDisplayString()}) ?? [..commandLine.CommandLineArguments]"; return model.IsRequiredOrInit - ? $" {assignment}," + ? $"{assignment}," : $"result.{assignment};"; } From e014e9127df57d38565f570b95849676b569296b Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 15:09:09 +0800 Subject: [PATCH 057/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 925f96cd..231e5bad 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -48,7 +48,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateArgumentPropertyCode)) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( - "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)", + "private 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)", From e67c87a1fe14dad136bbeb288bce194f8badee3c Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 15:23:48 +0800 Subject: [PATCH 058/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=9A=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/CommandObjectGeneratingModel.cs | 9 +++-- .../Models/GeneratingModelExtensions.cs | 38 ++++++++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs index e7632275..88628c14 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -61,18 +61,19 @@ public IEnumerable EnumeratePositiona public IEnumerable EnumerateEnumPropertyTypes() { var enums = new HashSet(SymbolEqualityComparer.Default); + foreach (var option in OptionProperties) { - if (option.Type.TypeKind is TypeKind.Enum) + if (option.Type.TryGetEnumType(out var enumTypeSymbol)) { - enums.Add(option.Type); + enums.Add(enumTypeSymbol); } } foreach (var value in PositionalArgumentProperties) { - if (value.Type.TypeKind is TypeKind.Enum) + if (value.Type.TryGetEnumType(out var enumTypeSymbol)) { - enums.Add(value.Type); + enums.Add(enumTypeSymbol); } } return enums; diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs index a3b0ec89..41dce3eb 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs @@ -15,30 +15,40 @@ internal static class GeneratingModelExtensions ); /// - /// 假定 是一个命令行对象中一个枚举属性的属性类型, - /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, - /// 此方法返回这个辅助类型的名称。 + /// 尝试判断 是否是一个枚举类型(含可空枚举类型)。 + /// 如果是,则返回 ,并通过 返回枚举类型本身。 + /// 否则返回 ,并将 设为 。 /// /// 命令行对象中一个枚举属性的属性类型。 + /// 如果返回值为 ,则为枚举类型本身;否则为 。 /// 辅助类型的名称。 - public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + public static bool TryGetEnumType(this ITypeSymbol symbol, out ITypeSymbol enumTypeSymbol) { - string typeName; - if (symbol is { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) { - typeName = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType + enumTypeSymbol = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType // 获取 Nullable 中的 T。 - ? namedType.TypeArguments[0].ToDisplayString() + ? namedType.TypeArguments[0] // 处理直接带有可空标记的类型 (int? 这种形式)。 - : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); - } - else - { - typeName = symbol.ToDisplayString(); + : typeSymbol; + return enumTypeSymbol.TypeKind is TypeKind.Enum; } + enumTypeSymbol = symbol; + return symbol.TypeKind is TypeKind.Enum; + } - return $"__GeneratedEnumArgument__{typeName.Replace('.', '_')}__"; + /// + /// 假定 是一个命令行对象中一个枚举属性的属性类型, + /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, + /// 此方法返回这个辅助类型的名称。 + /// + /// 命令行对象中一个枚举属性的属性类型。 + /// 辅助类型的名称。 + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + { + return symbol.TryGetEnumType(out var enumTypeSymbol) + ? $"__GeneratedEnumArgument__{enumTypeSymbol.ToDisplayString().Replace('.', '_')}__" + : symbol.ToDisplayString(); } public static string ToCommandValueTypeName(this CommandValueKind type) => type switch From 776e7ac50da32e4e6af53eb028d7d07a5b95b967 Mon Sep 17 00:00:00 2001 From: walterlv Date: Thu, 18 Sep 2025 15:34:54 +0800 Subject: [PATCH 059/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=B2=A1=E6=9C=89=E6=97=B6=E5=80=99=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 39 +++++++++++-------- .../Models/CommandObjectGeneratingModel.cs | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 231e5bad..86d5da65 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -45,7 +45,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod m => m .AddRawStatements($"return new {model.Namespace}.{model.GetBuilderTypeName()}(commandLine).Build();")) .AddRawMembers(model.OptionProperties.Select(GenerateArgumentPropertyCode)) - .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateArgumentPropertyCode)) + .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)", @@ -55,15 +55,16 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod m => GenerateMatchShortOptionCode(m, model)) .AddMethodDeclaration( "private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex)", - m => m - .AddRawStatements(GenerateMatchPositionalArgumentsCode(model)) - .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) + m => GenerateMatchPositionalArgumentsCode(m, model)) .AddMethodDeclaration( "private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value)", m => m - .AddBracketScope("switch (propertyIndex)", l => l - .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode)) - .AddRawStatements(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode)))) + .Condition(model.OptionProperties.Count > 0 || model.PositionalArgumentProperties.Count > 0, b => b + .AddBracketScope("switch (propertyIndex)", l => l + .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode)) + .AddRawStatements(model.EnumeratePositionalArgumentExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode)))) + .Otherwise(b => b.AddRawStatement("// 没有可赋值的属性。")) + .EndCondition()) .AddMethodDeclaration( $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)", m => GenerateBuildCoreCode(m, model)) @@ -135,9 +136,9 @@ private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDec .AddLineSeparator() .AddRawStatement("// 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。") .AddBracketScope("if (namingPolicy.SupportsPascalCase())", s => s - .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames()))))) + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames())))) + .AddLineSeparator()) .EndCondition() - .AddLineSeparator() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateLongOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) @@ -174,9 +175,9 @@ private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDe .AddDefaultStringComparisonIfNeeded(optionProperties) .AddLineSeparator() .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") - .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetShortNames())))) + .AddRawStatements(optionProperties.Select(x => GenerateOptionEqualsCode(x, x.GetShortNames()))) + .AddLineSeparator()) .EndCondition() - .AddLineSeparator() .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;"); static string GenerateOptionCaseCode(OptionalArgumentPropertyGeneratingModel model, IReadOnlyList names) @@ -210,14 +211,18 @@ static string GenerateOptionEqualsCode(OptionalArgumentPropertyGeneratingModel m } } - private string GenerateMatchPositionalArgumentsCode(CommandObjectGeneratingModel model) + private MethodDeclarationSourceTextBuilder GenerateMatchPositionalArgumentsCode(MethodDeclarationSourceTextBuilder builder, + CommandObjectGeneratingModel model) { var positionalArgumentProperties = model.PositionalArgumentProperties; - return positionalArgumentProperties.Count is 0 - ? "// 没有位置参数,无需匹配。" - : $$""" - {{string.Join("\n", positionalArgumentProperties.Select(x => GenerateMatchPositionalArgumentCode(x, x.Index, x.Length)))}} - """; + return builder + .Condition(positionalArgumentProperties.Count is 0, b => b + .AddRawStatement("// 没有位置参数,无需匹配。")) + .Otherwise(b => b + .AddRawStatements(positionalArgumentProperties.Select(x => GenerateMatchPositionalArgumentCode(x, x.Index, x.Length))) + .AddLineSeparator()) + .EndCondition() + .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;"); } private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGeneratingModel model, int index, int length) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs index 88628c14..9b860e19 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -36,7 +36,7 @@ public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) { } names => names.Count(x => x == ' ') + 1, }; - public IEnumerable EnumeratePositionalArgumentPropertiesExcludingSameNameOptions() + public IEnumerable EnumeratePositionalArgumentExcludingSameNameOptions() { var optionNames = OptionProperties.Select(x => x.PropertyName).ToList(); foreach (var positionalArgumentProperty in PositionalArgumentProperties) From b7d668e4bf7d8c5ab679012b9f1cce58f50c558e Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 13:15:41 +0800 Subject: [PATCH 060/193] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=8C=B9=E9=85=8D=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 86d5da65..b4414cb9 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -215,27 +215,35 @@ private MethodDeclarationSourceTextBuilder GenerateMatchPositionalArgumentsCode( CommandObjectGeneratingModel model) { var positionalArgumentProperties = model.PositionalArgumentProperties; + var matchAllProperty = positionalArgumentProperties.FirstOrDefault(x => x.Index is 0 && x.Length is int.MaxValue); return builder .Condition(positionalArgumentProperties.Count is 0, b => b - .AddRawStatement("// 没有位置参数,无需匹配。")) + .AddRawStatement("// 没有位置参数,无需匹配。") + .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) + .Condition(matchAllProperty is not null, b => b + .AddRawStatement($"// 属性 {matchAllProperty!.PropertyName} 覆盖了所有位置参数,直接匹配。") + .AddRawStatement($""" +return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{matchAllProperty.PropertyName}", {matchAllProperty.PropertyIndex}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); +""")) .Otherwise(b => b .AddRawStatements(positionalArgumentProperties.Select(x => GenerateMatchPositionalArgumentCode(x, x.Index, x.Length))) - .AddLineSeparator()) - .EndCondition() - .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;"); + .AddLineSeparator() + .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) + .EndCondition(); } private string GenerateMatchPositionalArgumentCode(PositionalArgumentPropertyGeneratingModel model, int index, int length) { return length switch { + <= 0 => "// 属性 {model.PropertyName} 的范围不包含任何位置参数,无法匹配。", 1 => $$""" if (argumentIndex is {{index}}) { return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); } """, - _ when index + length <= 0 => $$""" + _ when (index + length) is <= 0 or int.MaxValue => $$""" if (argumentIndex >= {{index}}) { return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("{{model.PropertyName}}", {{model.PropertyIndex}}, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); From 516d3cc06c737403607ea04b1854780a32e6de98 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 13:19:22 +0800 Subject: [PATCH 061/193] =?UTF-8?q?ToString=20=E6=97=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandLine.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index 3bb36f91..9c06d001 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -111,7 +111,7 @@ public T As(CommandObjectFactory factory) where T : notnull } /// - /// 输出传入的命令行参数字符串。 + /// 输出传入的命令行参数字符串。如果命令行参数中传入的是 URL,此方法会将 URL 转换为普通的命令行参数再输出。 /// /// 传入的命令行参数字符串。 [Pure] @@ -120,6 +120,16 @@ public override string ToString() return string.Join(" ", CommandLineArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); } + /// + /// 输出原始版本的传入的命令行参数字符串。如果命令行参数中传入的是 URL,此方法会原样输出 URL。 + /// + /// 原始传入的命令行参数字符串。 + [Pure] + public string ToRawString() + { + return string.Join(" ", RawArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); + } + CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); /// From 2c00ca421a4fb6df01074c1d47a82db4db13900b Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 13:39:33 +0800 Subject: [PATCH 062/193] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A7=E7=9A=84=20?= =?UTF-8?q?4.0=20=E7=9A=84=E4=B8=A4=E6=AD=A5=E8=A7=A3=E6=9E=90=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandLine.cs | 3 +- .../CommandLineParsingOptions.cs | 44 +- .../CommandRunnerBuilderExtensions.cs | 3 +- .../Compiler/CommandObjectFactory.cs | 5 - .../Compiler/ICommandHandlerCollection.cs | 84 ---- .../ICommandRunnerBuilder.cs | 36 -- .../LegacyCommandLine.cs | 397 ------------------ .../LegacyCommandLineParsingOptions.cs | 106 ----- .../LegacyCommandLineStyle.cs | 252 ----------- .../LegacyCommandRunner.cs | 212 ---------- .../LegacyCommandRunnerBuilderExtensions.cs | 200 --------- .../Utils/Collections/OptionDictionary.cs | 343 --------------- .../Utils/CommandLineConverter.cs | 137 +++++- .../DictionaryCommandHandlerCollection.cs | 38 -- ...neratedAssemblyCommandHandlerCollection.cs | 25 -- .../Utils/Parsers/CommandUrlParser.cs | 132 ------ .../Utils/Parsers/DotNetStyleParser.cs | 271 ------------ .../Utils/Parsers/FlexibleStyleParser.cs | 282 ------------- .../Utils/Parsers/GnuStyleParser.cs | 308 -------------- .../Utils/Parsers/ICommandLineParser.cs | 6 - .../Parsers/LegacyCommandLineParsedResult.cs | 20 - .../Utils/Parsers/PosixStyleParser.cs | 218 ---------- .../Utils/Parsers/PowerShellStyleParser.cs | 183 -------- .../Utils/Parsers/UrlStyleParser.cs | 321 -------------- 24 files changed, 133 insertions(+), 3493 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs delete mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLine.cs delete mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs delete mode 100644 src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs delete mode 100644 src/DotNetCampus.CommandLine/LegacyCommandRunner.cs delete mode 100644 src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index 9c06d001..073f94ce 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils; -using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; @@ -50,7 +49,7 @@ private CommandLine(IReadOnlyList arguments, CommandLineParsingOptions? { RawArguments = arguments; ParsingOptions = parsingOptions ?? CommandLineParsingOptions.Flexible; - (MatchedUrlScheme, _urlNormalizedArguments) = CommandUrlParser.TryNormalizeUrlArguments(arguments, ParsingOptions); + (MatchedUrlScheme, _urlNormalizedArguments) = CommandLineConverter.TryNormalizeUrlArguments(arguments, ParsingOptions); } /// diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 01d76b1d..4f43a17f 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -172,41 +172,17 @@ public readonly record struct CommandLineParsingOptions /// 这里的 "sample" 就是方案名。
/// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 /// - /// /// - /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
- ///
- /// 详细规则:
- /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
- /// 2. 参数部分以问号(?)开始,后面是键值对
- /// 3. 多个参数之间用(&)符号分隔
- /// 4. 每个参数的键值之间用等号(=)分隔
- /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
- /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
- /// 7. 支持无值参数,被视为布尔值true,如?enabled
- /// 8. 参数值为空字符串时保留等号,如?name=
- /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
- /// 10. 但在某些情况下,路径的前几个部分可能会被当作命令(含子命令),例如 myapp://open/file.txt 中,open 可能是命令,file.txt 是位置参数。具体解释为位置参数还是命令取决于应用的命令行处理器实现
- /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
+ /// + /// + /// 完整格式为 [scheme://][path1/path2][?option1=value1&option2=value2] + /// 整个解析过程不区分大小写 + /// scheme 为方案名,根据传入的命令行命名法进行匹配 + /// path1, path2 等路径会被视为命令和位置参数,具体是命令还是位置参数,跟普通命令行一样,优先匹配命令,剩下的全是位置参数 + /// option1, option2 等参数会被视为选项,只支持长选项;选项名根据传入的命令行命名法进行匹配 + /// 提取命令、位置参数、选项名和值时,会根据 URL 编码规则进行解码 + /// 支持布尔选项(无值选项),视为 true + /// /// - /// - /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) - /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 - /// myapp://user/profile?id=123&tab=info # 带层级路径 - /// sample://document/edit?id=42&mode=full # 多参数和路径组合 - /// - /// # 特殊字符与编码 - /// yourapp://search?q=hello%20world # 编码空格 - /// myapp://open?query=C%23%20programming # 特殊字符编码 - /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) - /// - /// # 无值和空值参数 - /// myapp://settings?debug # 无值参数(视为true) - /// yourapp://profile?name=&id=123 # 空字符串值 - /// - /// # 路径与命令示例 - /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 - /// myapp://open/file.txt?temporary=true # open 是命令,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 - /// /// public IReadOnlyList? SchemeNames { get; init; } diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index b3fd2d39..344c4466 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -166,8 +166,9 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICoreCommandRunnerBu /// 命令行执行器构造的链式调用。 /// 命令处理器集合的类型。 /// 命令行执行器构造的链式调用。 + [Obsolete("我们正在考虑更好的实现方式。此前这个依赖于模块初始化器,但我们正在用拦截器替换它。")] public static IAsyncCommandRunnerBuilder AddHandlers(this ICoreCommandRunnerBuilder builder) - where T : class, ICommandHandlerCollection, new() + // where T : class, ICommandHandlerCollection, new() { throw new NotImplementedException(); } diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs b/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs index 1f78660b..91d1a228 100644 --- a/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs +++ b/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs @@ -1,10 +1,5 @@ namespace DotNetCampus.Cli.Compiler; -/// -/// 从已解析的命令行参数创建命令数据模型或处理器的委托。 -/// -public delegate object LegacyCommandObjectCreator(LegacyCommandLine commandLine); - /// /// 从已解析的命令行参数创建命令数据模型或处理器的委托。 /// diff --git a/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs deleted file mode 100644 index 62723e38..00000000 --- a/src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace DotNetCampus.Cli.Compiler; - -/// -/// 管理一组命令处理器的集合,在命令匹配的情况下辅助执行对应的命令处理器。 -/// -public interface ICommandHandlerCollection -{ - /// - /// 尝试匹配一个命令处理器。 - /// - /// - /// 可能的命令名称。 - /// - /// 可能是空字符串,表示只匹配默认命令。 - /// 可能包含无空格的名称,表示只匹配主命令。 - /// 可能包含有空格的名称,表示匹配多级命令。 - /// - /// - /// 已解析的命令行参数。 - /// 匹配的命令处理器,如果没有匹配的命令处理器,则返回 - ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine); -} - -internal static class CommandHandlerCollectionMatcher -{ - /// - /// 尝试匹配一个命令处理器。 - /// - /// 已解析的命令行参数。 - /// - /// 这是来自命令行传入的参数,一般来说会多于实际需要的命令层级数。(会多几个位置参数进来,但我们也不知道这位置参数有没有可能是命令啊) - /// - /// 当没有任何命令匹配时,使用的默认命令处理器创建器。 - /// 尝试匹配命令时,使用此集合中的命令处理器创建器。 - /// 匹配的命令处理器,如果没有匹配的命令处理器,则返回 - internal static ICommandHandler? TryMatch( - this LegacyCommandLine commandLine, - string possibleCommandNames, - LegacyCommandObjectCreator? defaultHandlerCreator, - IReadOnlyDictionary commandHandlerCreators) - { - var caseSensitive = commandLine.ParsingOptions.CaseSensitive; - if (string.IsNullOrEmpty(possibleCommandNames)) - { - return (ICommandHandler?)defaultHandlerCreator?.Invoke(commandLine); - } - - var bestMatchLength = -1; - var bestMatch = new KeyValuePair("", null!); - foreach (var pair in commandHandlerCreators) - { - var names = pair.Key; - var creator = pair.Value; - - // 检查是否为精确匹配或完整的前缀匹配(后面跟空格或结束) - var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - bool isMatch = false; - - if (string.Equals(possibleCommandNames, names, comparison)) - { - // 完全匹配 - isMatch = true; - } - else if (possibleCommandNames.StartsWith(names, comparison)) - { - // 前缀匹配,但需要确保是完整单词匹配 - // 即命令名称后面必须是空格或字符串结束 - if (possibleCommandNames.Length > names.Length && possibleCommandNames[names.Length] == ' ') - { - isMatch = true; - } - } - - if (isMatch && names.Length > bestMatchLength) - { - bestMatchLength = names.Length; - bestMatch = new KeyValuePair(names, creator); - } - } - return bestMatch.Value is { } handlerCreator - ? (ICommandHandler)handlerCreator.Invoke(commandLine) - : null; - } -} diff --git a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs index 020d21b7..b985f3ce 100644 --- a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs +++ b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs @@ -1,41 +1,5 @@ namespace DotNetCampus.Cli; -/// -/// 命令行执行器构造器,用于链式创建命令行执行器。 -/// -public interface ILegacyCoreCommandRunnerBuilder -{ - /// - /// 获取或创建一个命令行执行器。 - /// - /// 命令行执行器。 - internal LegacyCommandRunner GetOrCreateRunner(); -} - -/// -/// 命令行执行器构造器,用于链式创建命令行执行器。 -/// -public interface ILegacyCommandRunnerBuilder : ILegacyCoreCommandRunnerBuilder -{ - /// - /// 以同步方式运行命令行处理器。 - /// - /// 将被执行的命令行处理器的返回值。 - int Run(); -} - -/// -/// 命令行执行器构造器,用于链式创建命令行执行器。 -/// -public interface ILegacyAsyncCommandRunnerBuilder : ILegacyCoreCommandRunnerBuilder -{ - /// - /// 以异步方式运行命令行处理器。 - /// - /// 将被执行的命令行处理器的返回值。 - Task RunAsync(); -} - /// /// 命令行执行器构造器,用于链式创建命令行执行器。 /// diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLine.cs b/src/DotNetCampus.CommandLine/LegacyCommandLine.cs deleted file mode 100644 index e517fe83..00000000 --- a/src/DotNetCampus.CommandLine/LegacyCommandLine.cs +++ /dev/null @@ -1,397 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics.Contracts; -using System.Globalization; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Utils; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli; - -/// -/// 为应用程序提供统一的命令行参数解析功能。 -/// -public class LegacyCommandLine : ILegacyCoreCommandRunnerBuilder -{ - /// - /// 获取此命令行解析类型所关联的命令行参数。 - /// - public IReadOnlyList CommandLineArguments { get; } - - /// - /// 获取解析此命令行时所使用的各种选项。 - /// - internal LegacyCommandLineParsingOptions ParsingOptions { get; } - - /// - /// 在特定的属性不指定时,默认应使用的大小写敏感性。 - /// - public bool DefaultCaseSensitive { get; } - - /// - /// 获取命令行参数中猜测的多级命令名称。 - /// 请注意,此字符串中可能包含空格,表示多级命令名称。也可能比预期的更长,包含后续的一部分位置参数,因为暂时还无法确定那些位置参数是否是命令名称。 - /// - /// - /// - /// # 对于以下命令: - /// do something --option value - /// # 本属性的值为 "do something"。 - /// # 对于以下命令: - /// do something /var/file --option value - /// # 本属性的值为 "do something"(因为 /var/file 可以提前判断出来不可能是命令) - /// # 可能存在三种情况: - /// # 1. do 和 something 都是位置参数。 - /// # 2. do 是命令,something 是位置参数。 - /// # 3. do 和 something 都是命令。 - /// - /// 此属性保存这个 something 的值,待后续决定使用处理器时,根据处理器是否要求有命令来决定这个词是否是位置参数。
- /// 另外,**特别强调**,此属性的值可能是命名变体,例如命令行传入 DoSomething 时,此属性则是 Do-Something。 - ///
- internal string PossibleCommandNames { get; } - - /// - /// 如果此命令行是从 Web 请求的 URL 中解析出来的,则此属性保存 URL 的 Scheme 部分。 - /// - private string? MatchedUrlScheme { get; } - - /// - /// 适用于选项的多值处理方式。 - /// - private MultiValueHandling OptionMultiValueHandling { get; } - - /// - /// 适用于位置参数的多值处理方式。 - /// - private MultiValueHandling PositionalArgumentsMultiValueHandling { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写敏感。 - /// - private OptionDictionary LongOptionValuesDefault { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写敏感。 - /// - private OptionDictionary LongOptionValuesCaseSensitive { get; } - - /// - /// 从命令行中解析出来的长名称选项。始终大小写不敏感。 - /// - private OptionDictionary LongOptionValuesIgnoreCase { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写敏感。 - /// - private OptionDictionary ShortOptionValuesDefault { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写敏感。 - /// - private OptionDictionary ShortOptionValuesCaseSensitive { get; } - - /// - /// 从命令行中解析出来的短名称选项。始终大小写不敏感。 - /// - private OptionDictionary ShortOptionValuesIgnoreCase { get; } - - /// - /// 从命令行中解析出来的位置参数。 - /// - /// - /// 注意,位置参数的前几个值可能是命令名称;这取决于 和实际处理器的命令。 - /// - /// # 对于以下命令: - /// do something --option value - /// # 可能存在三种情况: - /// # 1. do 和 something 都是位置参数。 - /// # 2. do 是命令,something 是位置参数。 - /// # 3. do 和 something 都是命令。 - /// - /// 如果处理器决定将 something 作为命令名称,那么当需要取出位置参数时,此属性的第一个值需要排除。 - /// - private ReadOnlyListRange PositionalArguments { get; } - - private LegacyCommandLine() - { - var options = OptionDictionary.Empty; - var arguments = new ReadOnlyListRange(); - CommandLineArguments = arguments; - ParsingOptions = LegacyCommandLineParsingOptions.Flexible; - DefaultCaseSensitive = false; - PossibleCommandNames = ""; - MatchedUrlScheme = null; - OptionMultiValueHandling = MultiValueHandling.First; - PositionalArgumentsMultiValueHandling = MultiValueHandling.First; - LongOptionValuesCaseSensitive = options; - LongOptionValuesIgnoreCase = options; - LongOptionValuesDefault = options; - ShortOptionValuesCaseSensitive = options; - ShortOptionValuesIgnoreCase = options; - ShortOptionValuesDefault = options; - PositionalArguments = arguments; - } - - private LegacyCommandLine(IReadOnlyList arguments, LegacyCommandLineParsingOptions? parsingOptions = null) - { - CommandLineArguments = arguments; - ParsingOptions = parsingOptions ?? LegacyCommandLineParsingOptions.Flexible; - DefaultCaseSensitive = parsingOptions?.CaseSensitive ?? false; - (MatchedUrlScheme, var result) = CommandLineConverter.ParseCommandLineArguments(arguments, parsingOptions); - PossibleCommandNames = result.PossibleCommandNames; - OptionMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.First : MultiValueHandling.Last; - PositionalArgumentsMultiValueHandling = MatchedUrlScheme is null ? MultiValueHandling.SpaceAll : MultiValueHandling.SlashAll; - LongOptionValuesCaseSensitive = result.LongOptions.ToOptionLookup(true); - LongOptionValuesIgnoreCase = result.LongOptions.ToOptionLookup(false); - LongOptionValuesDefault = DefaultCaseSensitive ? LongOptionValuesCaseSensitive : LongOptionValuesIgnoreCase; - ShortOptionValuesCaseSensitive = result.ShortOptions.ToOptionLookup(true); - ShortOptionValuesIgnoreCase = result.ShortOptions.ToOptionLookup(false); - ShortOptionValuesDefault = DefaultCaseSensitive ? ShortOptionValuesCaseSensitive : ShortOptionValuesIgnoreCase; - PositionalArguments = result.Arguments; - } - - /// - /// 解析命令行参数,并获得一个通用的命令行解析类型。 - /// - /// 命令行参数。 - /// 以此方式解析命令行参数。 - /// 统一的命令行参数解析中间类型。 - [Pure] - public static LegacyCommandLine Parse(IReadOnlyList args, LegacyCommandLineParsingOptions? parsingOptions = null) - { -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(args); -#else - if (args is null) - { - throw new ArgumentNullException(nameof(args)); - } -#endif - return args.Count is 0 - ? new LegacyCommandLine() - : new LegacyCommandLine(args, parsingOptions); - } - - /// - /// 解析一整行命令(所有参数被放在了同一个字符串中),并获得一个通用的命令行解析类型。 - /// - /// 一整行命令。 - /// 以此方式解析命令行参数。 - /// 统一的命令行参数解析中间类型。 - [Pure] - public static LegacyCommandLine Parse(string singleLineCommandLineArgs, LegacyCommandLineParsingOptions? parsingOptions = null) - { - var args = CommandLineConverter.SingleLineToList(singleLineCommandLineArgs); - return new LegacyCommandLine(args, parsingOptions); - } - - LegacyCommandRunner ILegacyCoreCommandRunnerBuilder.GetOrCreateRunner() => new(this); - - /// - /// 尝试将命令行参数转换为指定类型的实例。 - /// - /// 要转换的类型。 - /// 转换后的实例。 - [Pure] - public T As() where T : class => LegacyCommandRunner.CreateInstance(this); - - /// - /// 尝试将命令行参数转换为指定类型的实例。 - /// - /// 由拦截器传入的命令处理器创建方法。 - /// 要转换的类型。 - /// 转换后的实例。 - [Pure, EditorBrowsable(EditorBrowsableState.Never)] - public T As(LegacyCommandObjectCreator creator) where T : class => LegacyCommandRunner.CreateInstance(this, creator); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortOption) => GetShortOption(shortOption.ToString(CultureInfo.InvariantCulture)); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetShortOption(string shortOption) - { - return ShortOptionValuesDefault.TryGetValue(shortOption, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(string optionName) - { - return LongOptionValuesDefault.TryGetValue(optionName, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 短名称选项。 - /// 选项的名称。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortName, string longName) => - // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 - GetOption(shortName) - // 其次使用长名称。 - ?? GetOption(longName); - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char optionName, bool caseSensitive) => - GetShortOption(optionName.ToString(CultureInfo.InvariantCulture), caseSensitive); - - /// - /// 获取命令行参数中指定短名称的选项的值。 - /// - /// 短名称选项。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetShortOption(string shortOption, bool caseSensitive) - { - var optionValues = caseSensitive - ? ShortOptionValuesCaseSensitive - : ShortOptionValuesIgnoreCase; - return optionValues.TryGetValue(shortOption, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(string optionName, bool caseSensitive) - { - var optionValues = caseSensitive - ? LongOptionValuesCaseSensitive - : LongOptionValuesIgnoreCase; - return optionValues.TryGetValue(optionName, out var defaultValues) - ? new CommandLinePropertyValue(defaultValues, OptionMultiValueHandling) - : null; - } - - /// - /// 获取命令行参数中指定名称的选项的值。 - /// - /// 短名称选项。 - /// 选项的名称。 - /// 单独为此选项设置的大小写敏感性。 - /// 返回选项的值。当命令行未传入此参数时返回 - [Pure] - public CommandLinePropertyValue? GetOption(char shortName, string longName, bool caseSensitive) => - // 优先使用短名称(因为长名称可能是根据属性名猜出来的)。 - GetOption(shortName, caseSensitive) - // 其次使用长名称。 - ?? GetOption(longName, caseSensitive); - - /// - /// 获取命令行参数中位置参数的值。 - /// - /// 获取指定索引处的参数值。 - /// 从索引处获取参数值的最长长度。当大于 1 时,会将这些值合并为一个字符串。 - /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 - /// 位置参数的值。 - [Pure] - public CommandLinePropertyValue? GetPositionalArgument(int index, int length, string? commandNames = null) - { - var commandLevel = GetCommandLevel(commandNames); - var positionalArgumentsStartIndex = Math.Max(0, commandLevel); - positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); - var realIndex = index + positionalArgumentsStartIndex; - return realIndex < 0 || realIndex >= PositionalArguments.Count - ? null - : new CommandLinePropertyValue( - PositionalArguments.Slice(realIndex, - Math.Min(length, PositionalArguments.Count - realIndex)), PositionalArgumentsMultiValueHandling); - } - - /// - /// 获取命令行参数中所有位置参数值的集合。 - /// - /// 因为子命令会影响到位置参数的序号,所以如果存在命令和子命令,则需要传入所有多级命令共同组成的字符串。 - /// 命令行参数中位置参数值的集合。 - [Pure] - public IReadOnlyList GetPositionalArguments(string? commandNames = null) - { - var commandLevel = GetCommandLevel(commandNames); - var positionalArgumentsStartIndex = Math.Max(0, commandLevel); - positionalArgumentsStartIndex = Math.Min(positionalArgumentsStartIndex, commandLevel); - return PositionalArguments.Slice(positionalArgumentsStartIndex, PositionalArguments.Count - 1); - } - - /// - /// 根据某个特定的命令名称字符串,获取此字符串中包含了多少级命令。 - /// - /// 命令名称字符串。 - /// 命令的层级数。 - private int GetCommandLevel(string? commandNames) - { - var possibleCommandNames = PossibleCommandNames; - - // 如果没有命令,则不需要排除任何位置参数。 - if (string.IsNullOrEmpty(commandNames) || string.IsNullOrEmpty(possibleCommandNames)) - { - return 0; - } -#if !NETCOREAPP3_1_OR_GREATER - if (commandNames is null) - { - return 0; - } -#endif - if (commandNames.Length > possibleCommandNames.Length) - { - return 0; - } - if (!possibleCommandNames.StartsWith(commandNames, DefaultCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) - { - return 0; - } - // 计算 possibleCommandNames 中有多少个空格。 - var commandLevel = 1; - for (var i = 0; i < commandNames.Length; i++) - { - if (possibleCommandNames[i] == ' ') - { - commandLevel++; - } - } - return commandLevel; - } - - /// - /// 输出传入的命令行参数字符串。 - /// - /// 传入的命令行参数字符串。 - [Pure] - public override string ToString() - { - return MatchedUrlScheme is { } scheme - ? $"{scheme}://{string.Join("/", PositionalArguments)}?{string.Join("&", LongOptionValuesCaseSensitive.Select(x => $"{x.Key}={string.Join("&", x.Value)}"))}" - : string.Join(" ", CommandLineArguments.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); - } -} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs deleted file mode 100644 index cf9e9245..00000000 --- a/src/DotNetCampus.CommandLine/LegacyCommandLineParsingOptions.cs +++ /dev/null @@ -1,106 +0,0 @@ -namespace DotNetCampus.Cli; - -/// -/// 在解析命令行参数时,指定命令行参数的解析方式。 -/// -public readonly record struct LegacyCommandLineParsingOptions() -{ - /// - public static LegacyCommandLineParsingOptions Flexible => new LegacyCommandLineParsingOptions - { - Style = LegacyCommandLineStyle.Flexible, - CaseSensitive = false, - }; - - /// - public static LegacyCommandLineParsingOptions Gnu => new LegacyCommandLineParsingOptions - { - Style = LegacyCommandLineStyle.Gnu, - CaseSensitive = true, - }; - - /// - public static LegacyCommandLineParsingOptions Posix => new LegacyCommandLineParsingOptions - { - Style = LegacyCommandLineStyle.Posix, - CaseSensitive = true, - }; - - /// - public static LegacyCommandLineParsingOptions DotNet => new LegacyCommandLineParsingOptions - { - Style = LegacyCommandLineStyle.DotNet, - CaseSensitive = false, - }; - - /// - public static LegacyCommandLineParsingOptions PowerShell => new LegacyCommandLineParsingOptions - { - Style = LegacyCommandLineStyle.PowerShell, - CaseSensitive = false, - }; - - /// - /// 以此风格解析命令行参数。 - /// - /// - /// 不指定时会自动根据用户输入的命令行参数判断风格。 - /// - public LegacyCommandLineStyle Style { get; init; } - - /// - /// 默认是大小写不敏感的,设置此值为 可以让命令行参数大小写敏感。 - /// - /// - /// 当然,可以在单独的属性上设置大小写敏感,设置后将在那个属性上覆盖此默认值。不设置的属性会使用此默认值。 - /// - public bool CaseSensitive { get; init; } - - /// - /// 此命令行解析器支持从 Web 打开本地应用时传入的参数。
- /// 此属性指定用于 URI 协议注册的方案名(scheme name)。 - ///
- /// - /// - /// 例如:sample://open?url=DotNetCampus%20is%20a%20great%20team
- /// 这里的 "sample" 就是方案名。
- /// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 - ///
- /// /// - /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
- ///
- /// 详细规则:
- /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
- /// 2. 参数部分以问号(?)开始,后面是键值对
- /// 3. 多个参数之间用(&)符号分隔
- /// 4. 每个参数的键值之间用等号(=)分隔
- /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
- /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
- /// 7. 支持无值参数,被视为布尔值true,如?enabled
- /// 8. 参数值为空字符串时保留等号,如?name=
- /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
- /// 10. 但在某些情况下,路径的前几个部分可能会被当作命令(含子命令),例如 myapp://open/file.txt 中,open 可能是命令,file.txt 是位置参数。具体解释为位置参数还是命令取决于应用的命令行处理器实现
- /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
- ///
- /// - /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) - /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 - /// myapp://user/profile?id=123&tab=info # 带层级路径 - /// sample://document/edit?id=42&mode=full # 多参数和路径组合 - /// - /// # 特殊字符与编码 - /// yourapp://search?q=hello%20world # 编码空格 - /// myapp://open?query=C%23%20programming # 特殊字符编码 - /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) - /// - /// # 无值和空值参数 - /// myapp://settings?debug # 无值参数(视为true) - /// yourapp://profile?name=&id=123 # 空字符串值 - /// - /// # 路径与命令示例 - /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 - /// myapp://open/file.txt?temporary=true # open 是命令,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 - /// - ///
- public IReadOnlyList SchemeNames { get; init; } = []; -} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs b/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs deleted file mode 100644 index 3bc8fb29..00000000 --- a/src/DotNetCampus.CommandLine/LegacyCommandLineStyle.cs +++ /dev/null @@ -1,252 +0,0 @@ -namespace DotNetCampus.Cli; - -/// -/// 命令行参数的风格规范。 -/// 不同的命令行工具可能使用不同的参数风格,本枚举定义了常见的几种命令行参数风格。 -/// -public enum LegacyCommandLineStyle -{ - /// - /// 灵活风格。
- /// 根据实际传入的参数,自动识别并支持多种主流风格,包括 等风格。 - /// 适用于希望为用户提供更灵活的参数传递体验的工具。 - ///
- /// - /// 灵活风格是一种包容性最强的命令行参数风格,旨在让不熟悉命令行操作的用户也能轻松使用。它通过智能识别尝试理解用户输入的意图,支持多种参数格式共存。
- ///
- /// 详细规则:
- /// 1. 参数前缀支持多种形式:双破折线(--), 单破折线(-), 斜杠(/,仅 Windows)
- /// 2. 参数值分隔符兼容多种形式:空格、等号(=)、冒号(:)
- /// 3. 参数命名风格兼容kebab-case(--parameter-name)、PascalCase(-ParameterName)和camelCase
- /// 4. 默认大小写不敏感,便于初学者使用
- /// 5. 支持短选项(-a)和长选项(--parameter),优先识别长选项
- /// 6. 支持布尔开关参数,可不带值或使用true/false、yes/no、on/off等常见值
- /// 7. 支持位置参数,并可通过双破折号(--)标记位置参数的开始
- /// 8. 支持有限的短选项组合(-abc),但当发生歧义时优先解析为单个选项
- /// 9. 当特性之间发生冲突时,优先保留简单、直观的用法,牺牲高级但复杂的功能
- /// 10. 自动检测并处理常见的用户错误,如选项名称拼写错误提示最接近的选项
- /// 11. 允许不同风格在同一命令行中混合使用
- ///
- /// 不支持的特性(为避免冲突):
- /// 1. 短选项组合中的最后一个选项不能直接附带参数(如-abc value,c无法接收value作为参数)
- /// 2. 不支持POSIX风格中的特殊数字操作数形式(如-42表示数字42)
- ///
- /// - /// # 长选项示例(多种风格) - /// app --parameter value # GNU风格空格分隔 - /// app --parameter=value # GNU风格等号分隔 - /// app --parameter:value # DotNet风格冒号分隔 - /// app -Parameter value # PowerShell风格(Pascal命名) - /// app --param-name value # Kebab-case命名 - /// app --paramName value # CamelCase命名 - /// - /// # 短选项示例(兼容多种形式) - /// app -p value # 短选项空格分隔 - /// app -p=value # 短选项等号分隔 - /// app -p:value # 短选项冒号分隔 - /// app -pvalue # 短选项直接跟值(GNU风格) - /// - /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) - /// app /parameter value # 斜杠前缀长选项 - /// app /p value # 斜杠前缀短选项 - /// app /parameter:value # 斜杠前缀冒号分隔(类MSBuild) - /// - /// # 布尔开关参数 - /// app --enable # 不带值的布尔参数(视为true) - /// app --no-feature # 否定形式(视为false) - /// app --feature=false # 显式布尔值 - /// app --feature=off # 替代布尔值形式 - /// app -e # 短格式布尔参数 - /// - /// # 位置参数和混合用法 - /// app value1 --param value2 # 位置参数和命名参数混用 - /// app --param value -- -value1 --value2 # -- 后的内容视为位置参数 - /// app -a value1 --param-b value2 /c:value3 # 混合使用不同风格 - /// - /// # 大小写不敏感(便于初学者) - /// app --PARAMETER value # 等同于 --parameter value - /// app -P value # 等同于 -p value - /// - /// # 有限支持的短选项组合 - /// app -abc # 等同于 -a -b -c(所有都是布尔开关) - /// - ///
- Flexible, - - /// - /// GNU风格,支持长选项和短选项:
- /// 1. 双破折线(--) + 长选项名称,通过等号(=)或空格赋值
- /// 2. 单破折线(-) + 短选项字符,可以空格赋值,也可以紧跟参数值
- /// 3. 同时支持多个单字符选项合并(如-abc 表示 -a -b -c) - ///
- /// - /// GNU风格是现代命令行工具中最广泛采用的标准之一,包括大多数Linux工具和跨平台应用程序。
- ///
- /// 详细规则:
- /// 1. 长选项以双破折线(--)开头,后跟由字母、数字、连字符组成的选项名
- /// 2. 长选项参数可以用等号(=)连接或用空格分隔
- /// 3. 短选项以单破折线(-)开头,后跟单个字符
- /// 4. 短选项参数可以直接跟在选项字符后,无需空格
- /// 5. 短选项也可以用空格分隔参数,或用等号连接参数
- /// 6. 多个不需要参数的短选项可以合并(如 -abc 等同于 -a -b -c)
- /// 7. 合并的短选项中,最后一个短选项可以带参数(如 -abc value 中 -c 接收 value 参数)
- /// 8. 双破折号(--) 作为单独参数时表示选项结束标记,之后的所有内容都被视为位置参数
- /// - /// - /// # 长选项示例 - /// app --option=value # 长选项用等号赋值 - /// app --option value # 长选项用空格赋值 - /// app --enable-feature # 布尔类型长选项(不需要值) - /// app --no-color # 否定形式的布尔长选项 - /// - /// # 短选项示例 - /// app -o=value # 短选项用等号赋值 - /// app -o value # 短选项用空格赋值 - /// app -ovalue # 短选项直接跟参数值(无空格) - /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) - /// app -abc value # 合并短选项,其中 c 接收参数值 - /// - /// # 混合使用 - /// app value1 value2 --option value -f # 位置参数 + 长选项 + 短选项 - /// app --option value -- -value1 --value2 # -- 后的 -value1 和 --value2 被视为位置参数 - /// - ///
- Gnu, - - /// - /// POSIX/UNIX风格,类似GNU但更严格:
- /// 1. 支持 - 开头的短选项,单个字符
- /// 2. 短选项可以组合使用(-abc 表示 -a -b -c)
- /// 3. 需要参数的选项必须与参数分开或使用特定格式 - ///
- /// - /// POSIX风格是UNIX系统中规范的命令行参数格式,相比GNU风格更加严格和精简,许多传统UNIX工具遵循此规范。
- ///
- /// 详细规则:
- /// 1. 只支持短选项,以单破折线(-)开头,后跟单个字母
- /// 2. 短选项参数必须用空格与选项分隔(标准做法)
- /// 3. 不需要参数的短选项(布尔选项)可以组合在一起(如 -abc 等同于 -a -b -c)
- /// 4. 在组合的短选项中,通常不支持为最后一个选项提供参数(这点与GNU不同)
- /// 5. 标准POSIX不支持长选项(以--开头)
- /// 6. 有些遵循POSIX的工具允许用破折线后跟操作数而不是选项(如 -42 表示数字42)
- /// 7. 双破折号(--)作为选项终止符,之后的参数被当作操作数而非选项
- /// - /// - /// # 标准短选项 - /// app -o value # 短选项用空格赋值 - /// app -a # 布尔短选项 - /// app -abc # 多个布尔短选项合并(等同于 -a -b -c) - /// - /// # 选项结束标记 - /// app -a -- -b file.txt # -- 后的 -b 被视为文件名而非选项 - /// app -a -b -- -c # -a 和 -b 是选项,-c 是参数 - /// - /// # 位置参数 - /// app file1.txt -a file2.txt # file1.txt 和 file2.txt 是位置参数 - /// - ///
- Posix, - - /// - /// .NET CLI风格,使用冒号分隔参数:
- /// 1. 短选项形式为 -参数:值
- /// 2. 长选项可以是 --参数:值
- /// 3. 也支持斜杠前缀 /参数:值(仅 Windows 环境下可用) - ///
- /// - /// 这种风格在现代.NET工具链(dotnet CLI、NuGet、MSBuild等)和其他Microsoft工具中广泛使用。
- ///
- /// 详细规则:
- /// 1. 支持使用冒号(:)作为选项和参数值的分隔符
- /// 2. 短选项以单破折线(-)开头,后跟选项名,然后是冒号和参数值
- /// 3. 长选项以双破折线(--)开头,后跟选项名,然后是冒号和参数值
- /// 4. 也支持使用斜杠(/)作为选项前缀,仅在Windows环境中可用
- /// 5. 参数名可以是单个字母、多字符缩写或完整的单词,支持各种命名规范
- /// 6. 布尔选项通常不需要值,或使用true/false、on/off等值
- /// 7. 多个短选项一般不支持合并(与GNU/POSIX不同)
- /// 8. 某些.NET工具也接受等号(=)作为选项和值的分隔符
- /// - /// - /// # 短选项示例 - /// dotnet build -c:Release # 短选项冒号语法 - /// dotnet test -t:UnitTest # 短选项指定测试类别 - /// dotnet publish -o:./publish # 指定输出目录 - /// dotnet build -tl:off # 双字符短选项 - /// - /// # 长选项示例 - /// dotnet build --verbosity:minimal # 长选项冒号语法 - /// dotnet run --project:App1 # 指定项目 - /// msbuild --target:Rebuild # MSBuild长选项 - /// - /// # 不同命名风格 - /// dotnet build -Configuration:Release # PascalCase,单破折号 - /// dotnet build --Configuration:Release # PascalCase,双破折号 - /// dotnet build /Configuration:Release # PascalCase,斜杠前缀 - /// dotnet test --test-category:UnitTest # kebab-case,双破折号 - /// dotnet run --projectName:App1 # camelCase,双破折号 - /// - /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) - /// msbuild /p:Configuration=Release # MSBuild属性 - /// dotnet test /blame # 启用故障分析 - /// dotnet nuget push /source:feed # 指定源 - /// dotnet test /tl:off # 斜杠前缀的短选项 - /// - /// # 布尔选项 - /// dotnet build -m:1 # 最大并行度 - /// dotnet test --blame # 不带值的布尔选项 - /// dotnet build --no-restore # 否定形式的布尔选项 - /// - /// # 混合用法 - /// dotnet publish -c:Release --no-build -o:./bin - /// dotnet test -Framework:net8.0 --verbosity:normal /blame - /// - ///
- DotNet, - - /// - /// PowerShell风格,使用 - 开头,但参数名称通常是完整单词或驼峰形式:
- /// 1. 长参数形式为 -参数名 值
- /// 2. 支持不带值的开关参数(开关参数)
- /// 3. 支持参数名称缩写 - ///
- /// - /// PowerShell命令行风格在微软的PowerShell脚本语言和相关工具中使用,具有独特的参数处理方式。
- ///
- /// 详细规则:
- /// 1. 参数名称前使用单个破折线(-),后跟完整的参数名(通常是Pascal或Camel大小写)
- /// 2. 参数名称与值之间用空格分隔
- /// 3. 支持参数名称的部分匹配和自动补全(只要能唯一标识参数)
- /// 4. 支持位置参数(根据位置而非参数名赋值)
- /// 5. 布尔开关参数不需要显式值(存在即为true)
- /// 6. 可以使用冒号语法传递数组或哈希表值
- /// 7. 不支持GNU/POSIX风格的短选项合并
- /// 8. 支持使用双引号或单引号包围包含空格的参数值
- /// 9. 支持参数别名(一个参数可以有多个名称)
- /// - /// - /// # 基本参数用法 - /// Get-Process -Name chrome # 带值的标准参数 - /// New-Item -Path "C:\temp" -ItemType Directory # 多个参数 - /// - /// # 开关参数(布尔参数) - /// Remove-Item -Recurse -Force # 两个开关参数(无需值) - /// Copy-Item file.txt backup/ -Verbose # 启用详细输出 - /// - /// # 参数名称缩写 - /// Get-Process -n chrome # -n 是 -Name 的缩写 - /// Get-ChildItem -Recurse -Fo *.txt # -Fo 是 -Force 的缩写(只要能唯一识别) - /// - /// # 位置参数(无需指定参数名) - /// Get-Process chrome # 位置参数,等同于 -Name chrome - /// - /// # 数组参数 - /// Get-Process -Name chrome,firefox,edge # 逗号分隔的数组 - /// Get-Process -ComputerName "srv1","srv2" # 引号包围的数组元素 - /// - /// # 复杂值和高级用法 - /// New-Object -TypeName PSObject -Property @{Name="Value"; Count=1} # 哈希表参数 - /// Invoke-Command -ScriptBlock { Get-Process } -ComputerName Server01 # 脚本块参数 - /// - ///
- PowerShell, -} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs b/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs deleted file mode 100644 index c1e414ed..00000000 --- a/src/DotNetCampus.CommandLine/LegacyCommandRunner.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Collections.Concurrent; -using System.ComponentModel; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Handlers; - -namespace DotNetCampus.Cli; - -/// -/// 辅助 根据已解析的命令行参数执行对应的命令处理器。 -/// -public class LegacyCommandRunner : ILegacyCommandRunnerBuilder, ILegacyAsyncCommandRunnerBuilder -{ - private static ConcurrentDictionary CommandObjectCreationInfos { get; } = new( -#if NET5_0_OR_GREATER - ReferenceEqualityComparer.Instance -#endif - ); - - private readonly LegacyCommandLine _commandLine; - private readonly DictionaryCommandHandlerCollection _dictionaryCommandHandlers = new(); - private readonly ConcurrentDictionary _assemblyCommandHandlers = []; - - internal LegacyCommandRunner(LegacyCommandLine commandLine) - { - _commandLine = commandLine; - } - - internal LegacyCommandRunner(LegacyCommandRunner commandRunner) - { - _commandLine = commandRunner._commandLine; - } - - /// - /// 供源生成器调用,注册一个专门用来处理主命令(Main Command)或子命令/多级子命令(Sub Command)的命令处理器。 - /// - /// 关联的命令。 - /// 命令处理器的创建方法。 - /// 选项类型,或命令处理器类型,或任意类型。 - [EditorBrowsable(EditorBrowsableState.Never)] - public static void Register(string? commandNames, LegacyCommandObjectCreator creator) - where T : class - { - CommandObjectCreationInfos[typeof(T)] = new CommandObjectCreationInfo(commandNames, creator); - } - - /// - /// 创建一个命令处理器实例。 - /// - /// 已解析的命令行参数。 - /// 命令处理器的类型。 - /// 命令处理器实例。 - internal static T CreateInstance(LegacyCommandLine commandLine) - { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) - { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); - } - - return (T)info.Creator(commandLine); - } - - /// - /// 创建一个命令处理器实例。 - /// - /// 已解析的命令行参数。 - /// 命令处理器的创建方法。 - /// 命令处理器的类型。 - /// 命令处理器实例。 - internal static T CreateInstance(LegacyCommandLine commandLine, LegacyCommandObjectCreator creator) - { - return (T)creator(commandLine); - } - - LegacyCommandRunner ILegacyCoreCommandRunnerBuilder.GetOrCreateRunner() => this; - - /// - /// 添加一个命令处理器。 - /// - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal LegacyCommandRunner AddHandler() - where T : class, ICommandHandler - { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) - { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); - } - - _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => (T)info.Creator(cl)); - return this; - } - - /// - /// 添加一个命令处理器。 - /// - /// 由拦截器传入的的命令处理器的命令。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - [EditorBrowsable(EditorBrowsableState.Never)] - internal LegacyCommandRunner AddHandler(string? command, LegacyCommandObjectCreator creator) - where T : class, ICommandHandler - { - _dictionaryCommandHandlers.AddHandler(command, creator); - return this; - } - - /// - /// 添加一个命令处理器。 - /// - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal LegacyCommandRunner AddHandler(Func> handler) - where T : class - { - if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) - { - throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); - } - - _dictionaryCommandHandlers.AddHandler(info.CommandNames, cl => new TaskCommandHandler( - () => (T)info.Creator(cl), - handler)); - return this; - } - - /// - /// 添加一个命令处理器。 - /// - /// 由拦截器传入的的命令处理器的命令。 - /// 由拦截器传入的命令处理器创建方法。 - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 返回一个命令处理器构建器。 - internal LegacyCommandRunner AddHandler(string? command, LegacyCommandObjectCreator creator, Func> handler) - where T : class - { - _dictionaryCommandHandlers.AddHandler(command, cl => new TaskCommandHandler( - () => (T)creator(cl), - handler)); - return this; - } - - internal LegacyCommandRunner AddHandlers() - where T : ICommandHandlerCollection, new() - { - var c = new T(); - _assemblyCommandHandlers.TryAdd(c, c); - return this; - } - - private ICommandHandler? MatchHandler() - { - var possibleCommandNames = _commandLine.PossibleCommandNames; - - // 优先寻找单独添加的处理器。 - if (_dictionaryCommandHandlers.TryMatch(possibleCommandNames, _commandLine) is { } h1) - { - return h1; - } - - // 其次寻找程序集中自动搜集到的处理器。 - foreach (var handler in _assemblyCommandHandlers) - { - if (handler.Value.TryMatch(possibleCommandNames, _commandLine) is { } h2) - { - return h2; - } - } - - // 如果没有找到,那么很可能此命令没有命令名称,需要使用默认的处理器。 - if (_dictionaryCommandHandlers.TryMatch("", _commandLine) is { } h3) - { - return h3; - } - foreach (var handler in _assemblyCommandHandlers) - { - if (handler.Value.TryMatch("", _commandLine) is { } h4) - { - return h4; - } - } - - // 如果连默认的处理器都没有找到,说明根本没有能处理此命令的处理器。 - return null; - } - - /// - public int Run() - { - return RunAsync().Result; - } - - /// - public Task RunAsync() - { - var handler = MatchHandler(); - - if (handler is null) - { - throw new CommandNameNotFoundException( - $"No command handler found for command '{_commandLine.PossibleCommandNames}'. Please ensure that the command handler is registered correctly.", - _commandLine.PossibleCommandNames); - } - - return handler.RunAsync(); - } - - private readonly record struct CommandObjectCreationInfo(string? CommandNames, LegacyCommandObjectCreator Creator); -} diff --git a/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs deleted file mode 100644 index e5e01670..00000000 --- a/src/DotNetCampus.CommandLine/LegacyCommandRunnerBuilderExtensions.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.ComponentModel; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli; - -/// -/// 辅助创建命令行执行程序。 -/// -public static class LegacyCommandRunnerBuilderExtensions -{ - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder) - where T : class, ICommandHandler - { - return builder.GetOrCreateRunner() - .AddHandler(); - } - - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 由拦截器传入的的命令处理器的命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator) - where T : class, ICommandHandler - { - return builder.GetOrCreateRunner() - .AddHandler(command, creator); - } - - /// - public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Action handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(t => - { - handler(t); - return Task.FromResult(0); - }); - } - - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Action handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, t => - { - handler(t); - return Task.FromResult(0); - }); - } - - /// - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, Action handler) - where T : class - { - return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(handler); - } - - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Action handler) - where T : class - { - return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); - } - - /// - public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(t => Task.FromResult(handler(t))); - } - - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Func handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, t => Task.FromResult(handler(t))); - } - - /// - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, Func handler) - where T : class - { - return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(handler); - } - - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyAsyncCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Func handler) - where T : class - { - return (ILegacyAsyncCommandRunnerBuilder)((ILegacyCoreCommandRunnerBuilder)builder).AddHandler(command, creator, handler); - } - - /// - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(async t => - { - await handler(t); - return 0; - }); - } - - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Func handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, async t => - { - await handler(t); - return 0; - }); - } - - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, Func> handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(handler); - } - - /// - /// 添加一个命令处理器。 - /// - /// 命令行执行器构造的链式调用。 - /// 由拦截器传入的的命令处理器的命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 用于处理已解析的命令行参数的委托。 - /// 命令处理器的类型。 - /// 命令行执行器构造的链式调用。 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILegacyAsyncCommandRunnerBuilder AddHandler(this ILegacyCoreCommandRunnerBuilder builder, - string? command, LegacyCommandObjectCreator creator, Func> handler) - where T : class - { - return builder.GetOrCreateRunner() - .AddHandler(command, creator, handler); - } - - /// - /// 添加一个命令处理器集合。 - /// - /// 命令行执行器构造的链式调用。 - /// 命令处理器集合的类型。 - /// 命令行执行器构造的链式调用。 - public static ILegacyAsyncCommandRunnerBuilder AddHandlers(this ILegacyCoreCommandRunnerBuilder builder) - where T : ICommandHandlerCollection, new() - { - return builder.GetOrCreateRunner() - .AddHandlers(); - } - - /// - /// 添加支持 GNU 标准的命令行通用参数。这将在无参数,带 --help 参数和带 --version 参数时得到通用的响应。
- /// 考虑到几乎没有开发者认为这个方法的行为符合预期,我们移除了这个功能。 - ///
- /// 命令行执行器构造的链式调用。 - /// 命令行执行器构造的链式调用。 - /// 任何时候调用这个方法都会抛出这个异常。 - [Obsolete("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature.", true)] - public static ILegacyAsyncCommandRunnerBuilder AddStandardHandlers(this ILegacyCoreCommandRunnerBuilder builder) - { - throw new NotSupportedException("Considering that almost no developer thinks the behavior of this method meets expectations, we removed this feature."); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs b/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs deleted file mode 100644 index 9bf2aba6..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Collections/OptionDictionary.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace DotNetCampus.Cli.Utils.Collections; - -/// -/// 为命令行选项特别优化的字典。优化了无值/单值的内存占用和拷贝,优化了多种不同的选项命名风格,优化了大小写敏感性。 -/// -internal class OptionDictionary(bool caseSensitive) : IReadOnlyDictionary> -{ - public static OptionDictionary Empty { get; } = new OptionDictionary(true); - - private readonly List>> _optionValues = []; - - private readonly StringComparison _stringComparer = caseSensitive - ? StringComparison.Ordinal - : StringComparison.OrdinalIgnoreCase; - - private OptionDictionary(bool caseSensitive, List>> optionValues) : this(caseSensitive) - { - _optionValues = optionValues; - } - - public int Count => _optionValues.Count; - - public IReadOnlyList this[string key] - { - get - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - if (string.Equals(pair.Key, key, _stringComparer)) - { - return pair.Value; - } - } - - throw new KeyNotFoundException($"Option '{key}' not found."); - } - } - - public IEnumerable Keys - { - get - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - yield return pair.Key; - } - } - } - - public IEnumerable> Values - { - get - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - yield return pair.Value; - } - } - } - - public bool ContainsKey(string key) - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - if (string.Equals(pair.Key, key, _stringComparer)) - { - return true; - } - } - return false; - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out IReadOnlyList value) - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - if (string.Equals(pair.Key, key, _stringComparer)) - { - value = pair.Value; - return true; - } - } - - value = null; - return false; - } - - public void AddOption(LegacyOptionName optionName) - { - var optionNameText = optionName.ToString(); - var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); - if (index < 0) - { - _optionValues.Add(new KeyValuePair>(optionNameText, [])); - } - } - - public void AddValue(LegacyOptionName optionName, string value) - { - var optionNameText = optionName.ToString(); - var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); - if (index >= 0) - { - _optionValues[index] = new KeyValuePair>(optionNameText, _optionValues[index].Value.Add(value)); - } - else - { - _optionValues.Add(new KeyValuePair>(optionNameText, new SingleOptimizedList(value))); - } - } - - public void AddValues(LegacyOptionName optionName, IReadOnlyList values) - { - var optionNameText = optionName.ToString(); - var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); - if (index >= 0) - { - _optionValues[index] = new KeyValuePair>(optionNameText, _optionValues[index].Value.AddRange(values)); - } - else - { - _optionValues.Add(new KeyValuePair>(optionNameText, new SingleOptimizedList().AddRange(values))); - } - } - - public void UpdateValue(LegacyOptionName optionName, string value) - { - var optionNameText = optionName.ToString(); - var index = _optionValues.FindIndex(p => string.Equals(p.Key, optionNameText, _stringComparer)); - if (index >= 0) - { - _optionValues[index] = new KeyValuePair>(optionNameText, new SingleOptimizedList(value)); - } - else - { - _optionValues.Add(new KeyValuePair>(optionNameText, new SingleOptimizedList(value))); - } - } - - /// - /// 保留当前字典的所有内容,但返回一个新字典,使用指定的大小写敏感性来查询选项的值。 - /// - /// 新的大小写敏感性。 - /// 原有字典内容但新的查询方式的字典。 - public OptionDictionary ToOptionLookup(bool newCaseSensitive) - { - if (newCaseSensitive == caseSensitive) - { - return this; - } - - return new OptionDictionary(newCaseSensitive, _optionValues); - } - - public IEnumerator>> GetEnumerator() - { - for (var i = 0; i < _optionValues.Count; i++) - { - var pair = _optionValues[i]; - yield return new KeyValuePair>(pair.Key, pair.Value); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} - -internal readonly record struct LegacyOptionName(string Argument, Range Range) : IEnumerable -{ - public char this[int index] - { - get - { - var (offset, length) = Range.GetOffsetAndLength(Argument.Length); - var realIndex = index + offset; - if (realIndex < offset || realIndex >= offset + length) - { - throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range."); - } - return Argument[realIndex]; - } - } - -#if NET8_0_OR_GREATER - public ReadOnlySpan AsSpan() => Argument.AsSpan(Range); -#else - public ReadOnlySpan AsSpan() - { - var (offset, length) = Range.GetOffsetAndLength(Argument.Length); - return Argument.AsSpan(offset, length); - } -#endif - - public bool Equals(LegacyOptionName? other) - { - if (other is null) - { - return false; - } - - var (thisOffset, thisLength) = Range.GetOffsetAndLength(Argument.Length); - var (thatOffset, thatLength) = other.Value.Range.GetOffsetAndLength(other.Value.Argument.Length); - if (thisLength != thatLength) - { - return false; - } - - for (var i = 0; i < thisLength; i++) - { - if (Argument[thisOffset + i] != other.Value.Argument[thatOffset + i]) - { - return false; - } - } - - return true; - } - - public bool Equals(LegacyOptionName other, bool caseSensitive) - { - var (thisOffset, thisLength) = Range.GetOffsetAndLength(Argument.Length); - var (thatOffset, thatLength) = other.Range.GetOffsetAndLength(other.Argument.Length); - if (thisLength != thatLength) - { - return false; - } - - for (var i = 0; i < thisLength; i++) - { - var thisChar = Argument[thisOffset + i]; - var thatChar = other.Argument[thatOffset + i]; - if (thisChar != thatChar) - { - if (caseSensitive) - { - return false; - } - if (char.ToLowerInvariant(thisChar) != char.ToLowerInvariant(thatChar)) - { - return false; - } - } - } - - return true; - } - - public IEnumerator GetEnumerator() - { - var (offset, length) = Range.GetOffsetAndLength(Argument.Length); - for (var i = offset; i < offset + length; i++) - { - yield return Argument[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public override string ToString() => AsSpan().ToString(); - - public static implicit operator LegacyOptionName(string optionName) => new LegacyOptionName(optionName, Range.All); - - public static implicit operator LegacyOptionName(char optionName) => new LegacyOptionName(optionName.ToString(), Range.All); - - public static bool IsValidOptionName(ReadOnlySpan span) - { - if (span.Length == 0) - { - return false; - } - if (!char.IsLetterOrDigit(span[0])) - { - return false; - } - for (var i = 1; i < span.Length; i++) - { - var c = span[i]; - if (!(char.IsLetterOrDigit(c) || c is '-' or '_')) - { - return false; - } - } - return true; - } - - public static LegacyOptionName MakeKebabCase(ReadOnlySpan span, bool isUpperSeparator) - { - var name = NamingHelper.MakeKebabCase(span.ToString(), isUpperSeparator, false); - return new LegacyOptionName(name, Range.All); - } - - public static string MakeKebabCase(ReadOnlySpan span) - { - Span builder = stackalloc char[span.Length * 2]; - var needSeparator = false; - var actualBuilderCount = 0; - for (var i = 0; i < span.Length; i++) - { - var c = span[i]; - if (char.IsUpper(c)) - { - // 大写字母。 - if (needSeparator) - { - // 需要使用分隔符。 - builder[actualBuilderCount++] = '-'; - } - builder[actualBuilderCount++] = char.ToLowerInvariant(c); - } - else if (char.IsLetterOrDigit(c)) - { - // 无大小写,但可作为标识符的字符(对 char 来说也视为字母)。 - builder[actualBuilderCount++] = c; - needSeparator = i + 1 < span.Length && char.IsUpper(span[i + 1]); - } - else - { - // 其他字符,直接添加。 - builder[actualBuilderCount++] = c; - } - } - if (actualBuilderCount == 0) - { - return ""; - } - if (actualBuilderCount == builder.Length) - { - return builder.ToString(); - } -#if NETCOREAPP3_1_OR_GREATER - return new string(builder[..actualBuilderCount]); -#else - return builder[..actualBuilderCount].ToString(); -#endif - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 58d143be..81be177b 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -1,6 +1,4 @@ -using DotNetCampus.Cli.Utils.Parsers; - -namespace DotNetCampus.Cli.Utils; +namespace DotNetCampus.Cli.Utils; /// /// 命令行参数转换器。 @@ -65,23 +63,128 @@ internal static IReadOnlyList SingleLineToList(string singleLineCommandL return [..parts.Select(part => singleLineCommandLineArgs[part])]; } - public static (string? MatchedUrlScheme, LegacyCommandLineParsedResult Result) ParseCommandLineArguments( - IReadOnlyList arguments, LegacyCommandLineParsingOptions? parsingOptions) + /// + /// 尝试将命令行参数中的 URL 转换为普通的命令行参数列表。 + /// + /// 原始传入的命令行参数。 + /// 命令行解析选项。 + /// 如果传入的命令行参数中包含 URL,则返回转换后的命令行参数列表和 URL 的 Scheme 部分。 + internal static (string? MatchedUrlScheme, IReadOnlyList? UrlNormalizedArguments) TryNormalizeUrlArguments( + IReadOnlyList originalArguments, CommandLineParsingOptions options) { - var matchedUrlScheme = arguments.Count is 1 && parsingOptions?.SchemeNames is { Count: > 0 } schemeNames - ? schemeNames.FirstOrDefault(x => arguments[0].StartsWith($"{x}://", StringComparison.OrdinalIgnoreCase)) - : null; + if (originalArguments.Count is not 1 || options.SchemeNames is not { Count: > 0 } schemeNames) + { + return (null, null); + } - ICommandLineParser parser = (matchUrlScheme: matchedUrlScheme, parsingOptions?.Style) switch + var argument = originalArguments[0]; + foreach (var schemeName in schemeNames) { - ({ } scheme, _) => new UrlStyleParser(scheme), - (_, LegacyCommandLineStyle.Flexible) => new FlexibleStyleParser(), - (_, LegacyCommandLineStyle.Gnu) => new GnuStyleParser(), - (_, LegacyCommandLineStyle.Posix) => new PosixStyleParser(), - (_, LegacyCommandLineStyle.DotNet) => new DotNetStyleParser(), - (_, LegacyCommandLineStyle.PowerShell) => new PowerShellStyleParser(), - _ => new FlexibleStyleParser(), + if (argument.StartsWith($"{schemeName}://", StringComparison.OrdinalIgnoreCase)) + { + return (schemeName, NormalizeUrlArguments(schemeName, argument)); + } + } + return (null, null); + } + + /// + /// 将 URL 转换为普通的命令行参数列表。
+ ///
+ /// URL 的 Scheme 部分。 + /// URL 字符串。 + /// 普通的命令行参数列表。 + private static IReadOnlyList NormalizeUrlArguments(string schema, string argument) + { + // schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 + + var span = argument.AsSpan(); + + // 1. 跳过 schema:// + span = span[(schema.Length + 3)..]; + + // 2. 分成三个部分,分别解析。 + var questionMarkIndex = span.IndexOf('?'); + var fragmentIndex = span.IndexOf('#'); + var commandAndPositionalArgumentSpan = questionMarkIndex switch + { + -1 when fragmentIndex == -1 => span, + -1 => span[..fragmentIndex], + _ when fragmentIndex == -1 => span[..questionMarkIndex], + _ => span[..Math.Min(questionMarkIndex, fragmentIndex)], + }; + var optionSpan = questionMarkIndex switch + { + -1 => [], + _ when fragmentIndex == -1 => span[(questionMarkIndex + 1)..], + _ => span[(questionMarkIndex + 1)..fragmentIndex], + }; + var fragmentSpan = fragmentIndex switch + { + -1 => [], + _ => span[(fragmentIndex + 1)..], }; - return (matchedUrlScheme, parser.Parse(arguments)); + + // 3. 解析各个部分。 + var commandAndPositionalArgumentList = ParseCommandAndPositionalArguments(commandAndPositionalArgumentSpan); + var optionList = ParseOptions(optionSpan); + var fragmentList = ParseFragment(fragmentSpan); + + return [..commandAndPositionalArgumentList, ..optionList, ..fragmentList]; + } + + private static IReadOnlyList ParseCommandAndPositionalArguments(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + var parts = argument.ToString().Split(['/'], StringSplitOptions.RemoveEmptyEntries); + var result = new List(parts.Length); + foreach (var part in parts) + { + result.Add(Uri.UnescapeDataString(part)); + } + return result; + } + + private static IReadOnlyList ParseOptions(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + var parts = argument.ToString().Split(['&'], StringSplitOptions.RemoveEmptyEntries); + var result = new List(parts.Length); + foreach (var part in parts) + { + var equalSignIndex = part.IndexOf('='); + if (equalSignIndex == -1) + { + // 只有键,没有值 + result.Add($"--{Uri.UnescapeDataString(part)}"); + } + else + { + var key = part[..equalSignIndex]; + var value = part[(equalSignIndex + 1)..]; + result.Add($"--{Uri.UnescapeDataString(key)}={Uri.UnescapeDataString(value)}"); + } + } + + return result; + } + + private static IReadOnlyList ParseFragment(ReadOnlySpan argument) + { + if (argument.IsEmpty) + { + return []; + } + + // 片段部分直接作为一个位置参数 + return ["--fragment", Uri.UnescapeDataString(argument.ToString())]; } } diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs deleted file mode 100644 index 5c4d9cd1..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Concurrent; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Utils.Handlers; - -internal sealed class DictionaryCommandHandlerCollection : ICommandHandlerCollection -{ - private LegacyCommandObjectCreator? _defaultHandlerCreator; - private readonly ConcurrentDictionary _commandHandlerCreators = []; - - public void AddHandler(string? commandNames, LegacyCommandObjectCreator handlerCreator) - { - if ( -#if !NETCOREAPP3_1_OR_GREATER - commandNames is null || -#endif - string.IsNullOrEmpty(commandNames)) - { - if (_defaultHandlerCreator is not null) - { - throw new InvalidOperationException($"Duplicate default handler creator. Existed: {_defaultHandlerCreator}, new: {handlerCreator}"); - } - _defaultHandlerCreator = handlerCreator; - } - else - { - if (!_commandHandlerCreators.TryAdd(commandNames, handlerCreator)) - { - throw new InvalidOperationException($"Duplicate handler with command {commandNames}. Existed: {_commandHandlerCreators}, new: {handlerCreator}"); - } - } - } - - public ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine) - { - return commandLine.TryMatch(possibleCommandNames, _defaultHandlerCreator, _commandHandlerCreators); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs deleted file mode 100644 index 5e81e16a..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/GeneratedAssemblyCommandHandlerCollection.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Utils.Handlers; - -/// -/// 由源生成器继承,用于收集某个特定程序集中所有的命令处理器,然后统一处理。 -/// -public abstract class GeneratedAssemblyCommandHandlerCollection : ICommandHandlerCollection -{ - /// - /// 源生成器在构造函数中,为没有命令名称的命令处理器赋值。 - /// - protected LegacyCommandObjectCreator? Default { get; init; } - - /// - /// 源生成器在构造函数中,为有命令名称的命令处理器赋值。 - /// - protected Dictionary Creators { get; init; } = []; - - /// - public ICommandHandler? TryMatch(string possibleCommandNames, LegacyCommandLine commandLine) - { - return commandLine.TryMatch(possibleCommandNames, Default, Creators); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs deleted file mode 100644 index 327569bf..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandUrlParser.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -/// 如果命令行参数中传入的是 URL,则尝试将其转换为普通的命令行参数列表。 -/// -internal static class CommandUrlParser -{ - /// - /// 尝试将命令行参数中的 URL 转换为普通的命令行参数列表。 - /// - /// 原始传入的命令行参数。 - /// 命令行解析选项。 - /// 如果传入的命令行参数中包含 URL,则返回转换后的命令行参数列表和 URL 的 Scheme 部分。 - internal static (string? MatchedUrlScheme, IReadOnlyList? UrlNormalizedArguments) TryNormalizeUrlArguments( - IReadOnlyList originalArguments, CommandLineParsingOptions options) - { - if (originalArguments.Count is not 1 || options.SchemeNames is not { Count: > 0 } schemeNames) - { - return (null, null); - } - - var argument = originalArguments[0]; - foreach (var schemeName in schemeNames) - { - if (argument.StartsWith($"{schemeName}://", StringComparison.OrdinalIgnoreCase)) - { - return (schemeName, NormalizeUrlArguments(schemeName, argument)); - } - } - return (null, null); - } - - /// - /// 将 URL 转换为普通的命令行参数列表。
- ///
- /// URL 的 Scheme 部分。 - /// URL 字符串。 - /// 普通的命令行参数列表。 - private static IReadOnlyList NormalizeUrlArguments(string schema, string argument) - { - // schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 - - var span = argument.AsSpan(); - - // 1. 跳过 schema:// - span = span[(schema.Length + 3)..]; - - // 2. 分成三个部分,分别解析。 - var questionMarkIndex = span.IndexOf('?'); - var fragmentIndex = span.IndexOf('#'); - var commandAndPositionalArgumentSpan = questionMarkIndex switch - { - -1 when fragmentIndex == -1 => span, - -1 => span[..fragmentIndex], - _ when fragmentIndex == -1 => span[..questionMarkIndex], - _ => span[..Math.Min(questionMarkIndex, fragmentIndex)], - }; - var optionSpan = questionMarkIndex switch - { - -1 => [], - _ when fragmentIndex == -1 => span[(questionMarkIndex + 1)..], - _ => span[(questionMarkIndex + 1)..fragmentIndex], - }; - var fragmentSpan = fragmentIndex switch - { - -1 => [], - _ => span[(fragmentIndex + 1)..], - }; - - // 3. 解析各个部分。 - var commandAndPositionalArgumentList = ParseCommandAndPositionalArguments(commandAndPositionalArgumentSpan); - var optionList = ParseOptions(optionSpan); - var fragmentList = ParseFragment(fragmentSpan); - - return [..commandAndPositionalArgumentList, ..optionList, ..fragmentList]; - } - - private static IReadOnlyList ParseCommandAndPositionalArguments(ReadOnlySpan argument) - { - if (argument.IsEmpty) - { - return []; - } - - var parts = argument.ToString().Split(['/'], StringSplitOptions.RemoveEmptyEntries); - var result = new List(parts.Length); - foreach (var part in parts) - { - result.Add(Uri.UnescapeDataString(part)); - } - return result; - } - - private static IReadOnlyList ParseOptions(ReadOnlySpan argument) - { - if (argument.IsEmpty) - { - return []; - } - - var parts = argument.ToString().Split(['&'], StringSplitOptions.RemoveEmptyEntries); - var result = new List(parts.Length); - foreach (var part in parts) - { - var equalSignIndex = part.IndexOf('='); - if (equalSignIndex == -1) - { - // 只有键,没有值 - result.Add($"--{Uri.UnescapeDataString(part)}"); - } - else - { - var key = part[..equalSignIndex]; - var value = part[(equalSignIndex + 1)..]; - result.Add($"--{Uri.UnescapeDataString(key)}={Uri.UnescapeDataString(value)}"); - } - } - - return result; - } - - private static IReadOnlyList ParseFragment(ReadOnlySpan argument) - { - if (argument.IsEmpty) - { - return []; - } - - // 片段部分直接作为一个位置参数 - return ["--fragment", Uri.UnescapeDataString(argument.ToString())]; - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs deleted file mode 100644 index 7edde09f..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -internal sealed class DotNetStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = true; - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - var longOptions = new OptionDictionary(true); - var shortOptions = new OptionDictionary(true); - var possibleCommandNamesLength = 0; - List arguments = []; - - LegacyOptionName? lastOption = null; - var lastType = DotNetParsedType.Start; - - for (var i = 0; i < commandLineArguments.Count; i++) - { - var commandLineArgument = commandLineArguments[i]; - var result = DotNetArgument.Parse(commandLineArgument, lastType); - var tempLastType = lastType; - lastType = result.Type; - - if (result.Type is DotNetParsedType.CommandNameOrPositionalArgument) - { - lastOption = null; - possibleCommandNamesLength++; - var commandNameOrPositionalArgument = result.Value.ToString(); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is DotNetParsedType.PositionalArgument - or DotNetParsedType.PostPositionalArgument) - { - lastOption = null; - arguments.Add(result.Value.ToString()); - continue; - } - - if (result.Type is DotNetParsedType.LongOption) - { - lastOption = result.Option; - longOptions.AddOption(result.Option); - continue; - } - - if (result.Type is DotNetParsedType.LongOptionWithValue) - { - lastOption = null; - longOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is DotNetParsedType.ShortOption) - { - lastOption = result.Option; - shortOptions.AddOption(result.Option); - continue; - } - - if (result.Type is DotNetParsedType.ShortOptionWithValue) - { - lastOption = null; - shortOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is DotNetParsedType.OptionValue) - { - // 选项值,直接添加到参数列表中。 - var options = tempLastType switch - { - DotNetParsedType.LongOption => longOptions, - DotNetParsedType.ShortOption => shortOptions, - _ => throw new CommandLineParseException($"Argument value {result.Value.ToString()} does not belong to any option."), - }; - if (lastOption is { } option) - { - options.AddValue(option, result.Value.ToString()); - } - continue; - } - - if (result.Type is DotNetParsedType.PositionalArgumentSeparator) - { - lastOption = null; - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), - longOptions, - shortOptions, - arguments.ToReadOnlyList()); - } -} - -internal readonly ref struct DotNetArgument(DotNetParsedType type) -{ - public DotNetParsedType Type { get; } = type; - public LegacyOptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static DotNetArgument Parse(string argument, DotNetParsedType lastType) - { - var isPostPositionalArgument = lastType is DotNetParsedType.PositionalArgumentSeparator or DotNetParsedType.PostPositionalArgument; - var hasPrefix = -#if NET5_0_OR_GREATER - OperatingSystem.IsWindows() -#else - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) -#endif - ? argument.Length > 0 && (argument[0] is '-' or '/') - : argument.Length > 0 && argument[0] is '-'; - - if (!isPostPositionalArgument && hasPrefix) - { - if (argument.Length is 1) - { - // 只有一个破折号或斜杠,这在.NET CLI风格中通常被视为位置参数。 - return new DotNetArgument(DotNetParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (argument.Length is 2) - { - if (argument[0] is '-' && argument[1] is '-') - { - // 位置参数分隔符。 - return new DotNetArgument(DotNetParsedType.PositionalArgumentSeparator); - } - if (char.IsLetterOrDigit(argument[1])) - { - // 短选项。 - return new DotNetArgument(DotNetParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; - } - throw new CommandLineParseException($"Invalid option format at index [0, 1]: {argument}"); - } - - // 长选项。 - var isKebabCase = true; - var wordStartIndex = argument[1] is '-' ? 2 : 1; - var spans = argument.AsSpan(wordStartIndex); - for (var i = 0; i < spans.Length; i++) - { - var c = spans[i]; - if (i == 0 && !char.IsLetterOrDigit(c)) - { - // 长选项的第一个字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{i}, 2]: {argument}"); - } - if (i > 0 && char.IsUpper(c) && spans[i - 1] != '-') - { - // 遇到 PascalCase 或 camelCase,需要转换为 kebab-case。 - isKebabCase = false; - } - if (c is ':') - { - // 带值的长选项。--option:value - return new DotNetArgument(DotNetParsedType.LongOptionWithValue) - { - Option = isKebabCase - ? new LegacyOptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) - : LegacyOptionName.MakeKebabCase(spans[..i], DotNetStyleParser.ConvertPascalCaseToKebabCase), - Value = spans[(i + 1)..], - }; - } - } - // 单独的长选项。--option - return new DotNetArgument(DotNetParsedType.LongOption) - { - Option = isKebabCase - ? new LegacyOptionName(argument, Range.StartAt(wordStartIndex)) - : LegacyOptionName.MakeKebabCase(spans, DotNetStyleParser.ConvertPascalCaseToKebabCase), - }; - } - - if (lastType is DotNetParsedType.Start or DotNetParsedType.CommandNameOrPositionalArgument) - { - // 如果是第一个参数,则后续可能是命令名或位置参数。 - // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); - return new DotNetArgument(isValidName ? DotNetParsedType.CommandNameOrPositionalArgument : DotNetParsedType.PositionalArgument) - { - Value = argument.AsSpan(), - }; - } - - if (lastType is DotNetParsedType.PositionalArgument) - { - // 如果是位置参数,则后续必定是位置参数。 - return new DotNetArgument(DotNetParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is DotNetParsedType.OptionValue - or DotNetParsedType.LongOptionWithValue - or DotNetParsedType.ShortOptionWithValue) - { - // 如果前一个已经是选项值了,那么后一个是位置参数。 - return new DotNetArgument(DotNetParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is DotNetParsedType.PositionalArgumentSeparator or DotNetParsedType.PostPositionalArgument) - { - // 如果是位置参数分隔符或后置位置参数,则必定是后置位置参数。 - return new DotNetArgument(DotNetParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; - } - - // 其他情况,都是选项的值。 - return new DotNetArgument(DotNetParsedType.OptionValue) { Value = argument.AsSpan() }; - } -} - -internal enum DotNetParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// 长选项。--option -Option /option -tl /tl - /// - LongOption, - - /// - /// 带值的长选项。--option:value -Option:value /option:value -tl:off /tl:off - /// - LongOptionWithValue, - - /// - /// 短选项。-o /o - /// - ShortOption, - - /// - /// 带值的短选项。-o:value /o:value - /// - ShortOptionWithValue, - - /// - /// 选项值。value - /// - OptionValue, - - /// - /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 - /// - PositionalArgumentSeparator, - - /// - /// 后置的位置参数。 - /// - PostPositionalArgument, -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs deleted file mode 100644 index 2c8cf3c0..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System.Runtime.InteropServices; -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -internal sealed class FlexibleStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = true; - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - var longOptions = new OptionDictionary(true); - var shortOptions = new OptionDictionary(true); - var possibleCommandNamesLength = 0; - List arguments = []; - - OptionDictionary? lastOptions = null; - LegacyOptionName? lastOption = null; - var lastType = FlexibleParsedType.Start; - - for (var i = 0; i < commandLineArguments.Count; i++) - { - var commandLineArgument = commandLineArguments[i]; - var result = FlexibleArgument.Parse(commandLineArgument, lastType); - var tempLastType = lastType; - lastType = result.Type; - - if (result.Type is FlexibleParsedType.CommandNameOrPositionalArgument) - { - lastOptions = null; - lastOption = null; - possibleCommandNamesLength++; - var commandNameOrPositionalArgument = result.Value.ToString(); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is FlexibleParsedType.PositionalArgument - or FlexibleParsedType.PostPositionalArgument) - { - lastOptions = null; - lastOption = null; - arguments.Add(result.Value.ToString()); - continue; - } - - if (result.Type is FlexibleParsedType.LongOption) - { - lastOptions = longOptions; - lastOption = result.Option; - longOptions.AddOption(result.Option); - continue; - } - - if (result.Type is FlexibleParsedType.LongOptionWithValue) - { - lastOptions = null; - lastOption = null; - longOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is FlexibleParsedType.ShortOption) - { - lastOptions = shortOptions; - lastOption = result.Option; - shortOptions.AddOption(result.Option); - continue; - } - - if (result.Type is FlexibleParsedType.ShortOptionWithValue) - { - lastOptions = null; - lastOption = null; - shortOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is FlexibleParsedType.OptionValue) - { - // 选项值,直接添加到参数列表中。 - if (lastOptions is { } options && lastOption is { } option) - { - options.AddValue(option, result.Value.ToString()); - } - continue; - } - - if (result.Type is FlexibleParsedType.PositionalArgumentSeparator) - { - lastOption = null; - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), - longOptions, - shortOptions, - arguments.ToReadOnlyList()); - } -} - -internal readonly ref struct FlexibleArgument(FlexibleParsedType type) -{ - public FlexibleParsedType Type { get; } = type; - public LegacyOptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static FlexibleArgument Parse(string argument, FlexibleParsedType lastType) - { - var isPostPositionalArgument = lastType is FlexibleParsedType.PositionalArgumentSeparator or FlexibleParsedType.PostPositionalArgument; - var hasPrefix = -#if NET5_0_OR_GREATER - OperatingSystem.IsWindows() -#else - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) -#endif - ? argument.Length > 0 && (argument[0] is '-' or '/') - : argument.Length > 0 && argument[0] is '-'; - - if (!isPostPositionalArgument && hasPrefix) - { - if (argument.Length is 1) - { - // 只有一个破折号或斜杠,这在.NET CLI风格中通常被视为位置参数。 - return new FlexibleArgument(FlexibleParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (argument.Length is 2) - { - if (argument[0] is '-' && argument[1] is '-') - { - // 位置参数分隔符。 - return new FlexibleArgument(FlexibleParsedType.PositionalArgumentSeparator); - } - if (char.IsLetterOrDigit(argument[1])) - { - // 短选项。 - return new FlexibleArgument(FlexibleParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; - } - throw new CommandLineParseException($"Invalid option format at index [0, 1]: {argument}"); - } - - // 长选项。 - var isKebabCase = true; - var wordStartIndex = argument[1] is '-' ? 2 : 1; - var spans = argument.AsSpan(wordStartIndex); - for (var i = 0; i < spans.Length; i++) - { - var c = spans[i]; - if (i == 0 && !char.IsLetterOrDigit(c)) - { - // 长选项的第一个字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{i}, 2]: {argument}"); - } - if (i > 0 && char.IsUpper(c) && spans[i - 1] != '-') - { - // 遇到 PascalCase 或 camelCase,需要转换为 kebab-case。 - isKebabCase = false; - } - if (c is ':' or '=') - { - // 带值的长选项。--option:value --option=value - return new FlexibleArgument(FlexibleParsedType.LongOptionWithValue) - { - Option = isKebabCase - ? new LegacyOptionName(argument, new Range(wordStartIndex, i + wordStartIndex)) - : LegacyOptionName.MakeKebabCase(spans[..i], FlexibleStyleParser.ConvertPascalCaseToKebabCase), - Value = spans[(i + 1)..], - }; - } - } - // 单独的长选项。--option - return new FlexibleArgument(FlexibleParsedType.LongOption) - { - Option = isKebabCase - ? new LegacyOptionName(argument, Range.StartAt(wordStartIndex)) - : LegacyOptionName.MakeKebabCase(spans, FlexibleStyleParser.ConvertPascalCaseToKebabCase), - }; - } - - // 处理各种类型的位置参数 - if (lastType is FlexibleParsedType.Start or FlexibleParsedType.CommandNameOrPositionalArgument) - { - // 如果是第一个参数,则后续可能是命令名或位置参数。 - // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); - return new FlexibleArgument(isValidName ? FlexibleParsedType.CommandNameOrPositionalArgument : FlexibleParsedType.PositionalArgument) - { - Value = argument.AsSpan(), - }; - } - - if (lastType is FlexibleParsedType.PositionalArgument) - { - // 如果是位置参数,则后续必定是位置参数。 - return new FlexibleArgument(FlexibleParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is FlexibleParsedType.OptionValue) - { - // 如果前一个已经是选项值了,那么后一个是位置参数。 - return new FlexibleArgument(FlexibleParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is FlexibleParsedType.LongOptionWithValue - or FlexibleParsedType.ShortOptionWithValue) - { - // 如果前一个已经是带有值的选项了,那么后一个是位置参数。 - return new FlexibleArgument(FlexibleParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is FlexibleParsedType.PositionalArgumentSeparator or FlexibleParsedType.PostPositionalArgument) - { - // 如果是位置参数分隔符或后置位置参数,则必定是后置位置参数。 - return new FlexibleArgument(FlexibleParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; - } - - // 其他情况,都是选项的值。 - return new FlexibleArgument(FlexibleParsedType.OptionValue) { Value = argument.AsSpan() }; - } -} - -internal enum FlexibleParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// 长选项。--option -Option /option -tl /tl - /// - LongOption, - - /// - /// 带值的长选项。--option:value -Option:value /option:value -tl:off /tl:off - /// - LongOptionWithValue, - - /// - /// 短选项。-o /o - /// - ShortOption, - - /// - /// 带值的短选项。-o:value /o:value - /// - ShortOptionWithValue, - - /// - /// 多个短选项。-abc - /// - MultiShortOptions, - - /// - /// 选项值。value - /// - OptionValue, - - /// - /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 - /// - PositionalArgumentSeparator, - - /// - /// 后置的位置参数。 - /// - PostPositionalArgument, -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs deleted file mode 100644 index a771a29f..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/GnuStyleParser.cs +++ /dev/null @@ -1,308 +0,0 @@ -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -internal sealed class GnuStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = false; - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - var longOptions = new OptionDictionary(true); - var shortOptions = new OptionDictionary(true); - var possibleCommandNamesLength = 0; - List arguments = []; - - LegacyOptionName? lastOption = null; - var lastType = GnuParsedType.Start; - var shortLowPriorityOptions = new Dictionary(); - - for (var i = 0; i < commandLineArguments.Count; i++) - { - var commandLineArgument = commandLineArguments[i]; - var result = GnuArgument.Parse(commandLineArgument, lastType); - var tempLastType = lastType; - lastType = result.Type; - - if (result.Type is GnuParsedType.CommandNameOrPositionalArgument) - { - lastOption = null; - possibleCommandNamesLength++; - var commandNameOrPositionalArgument = result.Value.ToString(); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is GnuParsedType.PositionalArgument - or GnuParsedType.PostPositionalArgument) - { - lastOption = null; - arguments.Add(result.Value.ToString()); - continue; - } - - if (result.Type is GnuParsedType.LongOption) - { - lastOption = result.Option; - longOptions.AddOption(result.Option); - continue; - } - - if (result.Type is GnuParsedType.LongOptionWithValue) - { - lastOption = null; - longOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is GnuParsedType.ShortOption) - { - lastOption = result.Option; - shortOptions.AddOption(result.Option); - continue; - } - - if (result.Type is GnuParsedType.ShortOptionWithValue) - { - lastOption = null; - shortOptions.AddValue(result.Option, result.Value.ToString()); - continue; - } - - if (result.Type is GnuParsedType.MultiShortOptions) - { - lastOption = null; - foreach (var shortOption in result.Option) - { - shortOptions.AddOption(shortOption); - } - continue; - } - - if (result.Type is GnuParsedType.MultiShortOptionsOrShortOptionWithValue) - { - lastOption = null; - foreach (var shortOption in result.Option) - { - shortOptions.AddOption(shortOption); - } - shortLowPriorityOptions[result.Option[0].ToString()] = result.Value.ToString(); - continue; - } - - if (result.Type is GnuParsedType.OptionValue) - { - // 选项值,直接添加到参数列表中。 - var options = tempLastType switch - { - GnuParsedType.LongOption => longOptions, - GnuParsedType.ShortOption => shortOptions, - _ => throw new CommandLineParseException($"Argument value {result.Value.ToString()} does not belong to any option."), - }; - if (lastOption is { } option) - { - options.AddValue(option, result.Value.ToString()); - } - continue; - } - - if (result.Type is GnuParsedType.PositionalArgumentSeparator) - { - lastOption = null; - } - } - - // 最后,将潜在可能的短选项值添加到短选项中。-abc 其中 a 为选项,bc 为值。 - foreach (var pair in shortLowPriorityOptions) - { - if (!shortOptions.ContainsKey(pair.Key)) - { - shortOptions.AddValue(pair.Key, pair.Value); - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), - longOptions, - shortOptions, - arguments.ToReadOnlyList()); - } -} - -internal readonly ref struct GnuArgument(GnuParsedType type) -{ - public GnuParsedType Type { get; } = type; - public LegacyOptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static GnuArgument Parse(string argument, GnuParsedType lastType) - { - var isPostPositionalArgument = lastType is GnuParsedType.PositionalArgumentSeparator or GnuParsedType.PostPositionalArgument; - - if (!isPostPositionalArgument && argument is ['-', '-', ..]) - { - if (argument.Length is 2) - { - // 位置参数分隔符。 - return new GnuArgument(GnuParsedType.PositionalArgumentSeparator); - } - - // 长选项。 - var spans = argument.AsSpan(2); - for (var i = 0; i < spans.Length; i++) - { - if (i == 0 && !char.IsLetterOrDigit(spans[i])) - { - // 长选项的第一个字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{i}, 2]: {argument}"); - } - if (spans[i] == '=') - { - // 带值的长选项。--option=value - return new GnuArgument(GnuParsedType.LongOptionWithValue) - { Option = new LegacyOptionName(argument, new Range(2, i + 2)), Value = spans[(i + 1)..] }; - } - } - // 单独的长选项。--option - return new GnuArgument(GnuParsedType.LongOption) { Option = new LegacyOptionName(argument, Range.StartAt(2)) }; - } - - if (!isPostPositionalArgument && argument is ['-', _, ..]) - { - if (argument.Length is 2) - { - if (!char.IsLetterOrDigit(argument[1])) - { - // 短选项字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{argument.Length}, 1]: {argument}"); - } - // 单独的短选项。 - return new GnuArgument(GnuParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; - } - - var spans = argument.AsSpan(1); - for (var i = 0; i < spans.Length; i++) - { - if (i == 0 && !char.IsLetterOrDigit(spans[i])) - { - // 短选项的第一个字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{i}, 1]: {argument}"); - } - if (spans[i] == '=') - { - // 带值的短选项。 - return new GnuArgument(GnuParsedType.ShortOptionWithValue) - { Option = new LegacyOptionName(argument, new Range(1, 2)), Value = spans[2..] }; - } - if (!char.IsLetterOrDigit(spans[i])) - { - // 包含非字母或数字,说明必定是带值的短选项。-o1.txt - return new GnuArgument(GnuParsedType.ShortOptionWithValue) { Option = new LegacyOptionName(argument, new Range(1, 2)), Value = spans[1..] }; - } - } - // 多个短选项,或者带值的短选项。 - return new GnuArgument(GnuParsedType.MultiShortOptionsOrShortOptionWithValue) - { Option = new LegacyOptionName(argument, Range.StartAt(1)), Value = spans[1..] }; - } - - if (lastType is GnuParsedType.Start or GnuParsedType.CommandNameOrPositionalArgument) - { - // 如果是第一个参数,则后续可能是命令名或位置参数。 - // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); - return new GnuArgument(isValidName ? GnuParsedType.CommandNameOrPositionalArgument : GnuParsedType.PositionalArgument) - { - Value = argument.AsSpan(), - }; - } - - if (lastType is GnuParsedType.PositionalArgument) - { - // 如果是位置参数,则必定是位置参数。 - return new GnuArgument(GnuParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is GnuParsedType.OptionValue - or GnuParsedType.LongOptionWithValue - or GnuParsedType.ShortOptionWithValue - or GnuParsedType.MultiShortOptionsOrShortOptionWithValue) - { - // 如果前一个已经是选项值了,那么后一个是位置参数。 - return new GnuArgument(GnuParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is GnuParsedType.PositionalArgumentSeparator or GnuParsedType.PostPositionalArgument) - { - // 如果是位置参数分隔符或后置位置参数,则必定是后置位置参数。 - return new GnuArgument(GnuParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; - } - - // 其他情况,都是选项的值。 - return new GnuArgument(GnuParsedType.OptionValue) { Value = argument.AsSpan() }; - } -} - -internal enum GnuParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// 长选项。--long-option - /// - LongOption, - - /// - /// 带值的长选项。--long-option=value - /// - LongOptionWithValue, - - /// - /// 短选项。-o - /// - ShortOption, - - /// - /// 带值的短选项。-o=value - /// - ShortOptionWithValue, - - /// - /// 多个短选项。-abc - /// - MultiShortOptions, - - /// - /// 多个短选项,也可能是带值的短选项。-abc -o1.txt - /// - MultiShortOptionsOrShortOptionWithValue, - - /// - /// 选项值。value - /// - OptionValue, - - /// - /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 - /// - PositionalArgumentSeparator, - - /// - /// 后置的位置参数。 - /// - PostPositionalArgument, -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs deleted file mode 100644 index f3f07573..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DotNetCampus.Cli.Utils.Parsers; - -internal interface ICommandLineParser -{ - LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments); -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs deleted file mode 100644 index 1e829b5f..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/LegacyCommandLineParsedResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -internal readonly record struct LegacyCommandLineParsedResult( - string PossibleCommandNames, - OptionDictionary LongOptions, - OptionDictionary ShortOptions, - ReadOnlyListRange Arguments) -{ - public static string MakePossibleCommandNames(IEnumerable possibleCommandNames, bool isUpperSeparator) - { - return string.Join(" ", possibleCommandNames.Select(x => NamingHelper.MakeKebabCase(x, isUpperSeparator, false))); - } - - public static string MakePossibleCommandNames(IEnumerable commandLineArguments, int possibleCommandNamesLength, bool isUpperSeparator) - { - return string.Join(" ", commandLineArguments.Take(possibleCommandNamesLength).Select(x => NamingHelper.MakeKebabCase(x, isUpperSeparator, false))); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs deleted file mode 100644 index 39e63f51..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs +++ /dev/null @@ -1,218 +0,0 @@ -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -internal sealed class PosixStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = false; - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - var shortOptions = new OptionDictionary(true); - var possibleCommandNamesLength = 0; - List arguments = []; - - LegacyOptionName? lastOption = null; - var lastType = PosixParsedType.Start; - - for (var i = 0; i < commandLineArguments.Count; i++) - { - var commandLineArgument = commandLineArguments[i]; - var result = PosixArgument.Parse(commandLineArgument, lastType); - var tempLastType = lastType; - lastType = result.Type; - - if (result.Type is PosixParsedType.CommandNameOrPositionalArgument) - { - lastOption = null; - possibleCommandNamesLength++; - var commandNameOrPositionalArgument = result.Value.ToString(); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is PosixParsedType.PositionalArgument - or PosixParsedType.PostPositionalArgument) - { - lastOption = null; - arguments.Add(result.Value.ToString()); - continue; - } - - if (result.Type is PosixParsedType.ShortOption) - { - lastOption = result.Option; - shortOptions.AddOption(result.Option); - continue; - } - - if (result.Type is PosixParsedType.MultiShortOptions) - { - lastOption = null; - foreach (var shortOption in result.Option) - { - shortOptions.AddOption(shortOption); - } - continue; - } - - if (result.Type is PosixParsedType.OptionValue) - { - // 选项值,直接添加到参数列表中。 - var options = tempLastType switch - { - PosixParsedType.ShortOption => shortOptions, - _ => throw new CommandLineParseException($"Argument value {result.Value.ToString()} does not belong to any option."), - }; - if (lastOption is { } option) - { - options.AddValue(option, result.Value.ToString()); - lastOption = null; - } - continue; - } - - if (result.Type is PosixParsedType.PositionalArgumentSeparator) - { - lastOption = null; - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), - OptionDictionary.Empty, // POSIX 风格不支持长选项 - shortOptions, - arguments.ToReadOnlyList()); - } -} - -internal readonly ref struct PosixArgument(PosixParsedType type) -{ - public PosixParsedType Type { get; } = type; - public LegacyOptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static PosixArgument Parse(string argument, PosixParsedType lastType) - { - var isPostPositionalArgument = lastType is PosixParsedType.PositionalArgumentSeparator or PosixParsedType.PostPositionalArgument; - - if (!isPostPositionalArgument && argument is "--") - { - // 位置参数分隔符。 - return new PosixArgument(PosixParsedType.PositionalArgumentSeparator); - } - - if (!isPostPositionalArgument && argument is ['-', '-', ..]) - { - // POSIX 风格不支持长选项 - throw new CommandLineParseException($"Long options (starting with '--') are not supported in POSIX style: {argument}"); - } - - if (!isPostPositionalArgument && argument is ['-', _, ..]) - { - if (argument.Length is 2) - { - if (!char.IsLetterOrDigit(argument[1])) - { - // 短选项字符必须是字母或数字。 - throw new CommandLineParseException($"Invalid option format at index [{argument.Length}, 1]: {argument}"); - } - // 单独的短选项。 - return new PosixArgument(PosixParsedType.ShortOption) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; - } - - // 检查所有字符是否都是有效的选项字符 - for (var i = 1; i < argument.Length; i++) - { - if (!char.IsLetterOrDigit(argument[i])) - { - throw new CommandLineParseException($"Invalid option character in POSIX style: {argument[i]} in {argument}"); - } - } - - // 多个短选项,如 -abc - return new PosixArgument(PosixParsedType.MultiShortOptions) { Option = new LegacyOptionName(argument, Range.StartAt(1)) }; - } - - if (lastType is PosixParsedType.Start or PosixParsedType.CommandNameOrPositionalArgument) - { - // 如果是第一个参数,则后续可能是命令名或位置参数。 - // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); - return new PosixArgument(isValidName ? PosixParsedType.CommandNameOrPositionalArgument : PosixParsedType.PositionalArgument) - { - Value = argument.AsSpan(), - }; - } - - if (lastType is PosixParsedType.PositionalArgument) - { - // 如果上一个是位置参数,则这个也是位置参数。 - return new PosixArgument(PosixParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is PosixParsedType.OptionValue) - { - // 如果前一个已经是选项值了,那么后一个是位置参数。 - return new PosixArgument(PosixParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is PosixParsedType.PositionalArgumentSeparator or PosixParsedType.PostPositionalArgument) - { - // 如果是位置参数分隔符或后置位置参数,则必定是后置位置参数。 - return new PosixArgument(PosixParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; - } if (lastType is PosixParsedType.MultiShortOptions) - { - // 在POSIX风格中,组合短选项后面不能直接跟参数值 - throw new CommandLineParseException($"Combined short options cannot have parameters in POSIX style: {argument}"); - } - - // 其他情况,是单个短选项的值。 - return new PosixArgument(PosixParsedType.OptionValue) { Value = argument.AsSpan() }; - } -} - -internal enum PosixParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// 短选项。-o - /// - ShortOption, - - /// - /// 多个短选项。-abc - /// - MultiShortOptions, - - /// - /// 选项值。value - /// - OptionValue, - - /// - /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 - /// - PositionalArgumentSeparator, - - /// - /// 后置的位置参数。 - /// - PostPositionalArgument, -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs deleted file mode 100644 index 5de08f89..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs +++ /dev/null @@ -1,183 +0,0 @@ -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -internal sealed class PowerShellStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = true; - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - var longOptions = new OptionDictionary(true); - var possibleCommandNamesLength = 0; - List arguments = []; - - LegacyOptionName? lastOption = null; - var lastType = PowerShellParsedType.Start; - - for (var i = 0; i < commandLineArguments.Count; i++) - { - var commandLineArgument = commandLineArguments[i]; - var result = PowerShellArgument.Parse(commandLineArgument, lastType); - lastType = result.Type; - - if (result.Type is PowerShellParsedType.CommandNameOrPositionalArgument) - { - lastOption = null; - possibleCommandNamesLength++; - var commandNameOrPositionalArgument = result.Value.ToString(); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is PowerShellParsedType.PositionalArgument - or PowerShellParsedType.PostPositionalArgument) - { - lastOption = null; - arguments.Add(result.Value.ToString()); - continue; - } - - if (result.Type is PowerShellParsedType.Option) - { - lastOption = result.Option; - longOptions.AddOption(result.Option); - continue; - } - - if (result.Type is PowerShellParsedType.OptionValue) - { - // 选项值 - if (lastOption is { } option) - { - longOptions.AddValue(option, result.Value.ToString()); - } - continue; - } - - if (result.Type is PowerShellParsedType.PositionalArgumentSeparator) - { - lastOption = null; - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(commandLineArguments, possibleCommandNamesLength, ConvertPascalCaseToKebabCase), - longOptions, - // PowerShell 风格不使用短选项,所以直接使用空字典。 - OptionDictionary.Empty, - arguments.ToReadOnlyList()); - } -} - -internal readonly ref struct PowerShellArgument(PowerShellParsedType type) -{ - public PowerShellParsedType Type { get; } = type; - public LegacyOptionName Option { get; private init; } - public ReadOnlySpan Value { get; private init; } - - public static PowerShellArgument Parse(string argument, PowerShellParsedType lastType) - { - var isPostPositionalArgument = lastType is PowerShellParsedType.PositionalArgumentSeparator or PowerShellParsedType.PostPositionalArgument; - - if (!isPostPositionalArgument && argument is "--") - { - // 位置参数分隔符。 - return new PowerShellArgument(PowerShellParsedType.PositionalArgumentSeparator); - } - - if (!isPostPositionalArgument && argument.StartsWith( -#if NETCOREAPP3_1_OR_GREATER - '-' -#else - "-" -#endif - ) && argument.Length > 1 && !char.IsDigit(argument[1])) - { - // PowerShell 风格的选项 (-ParameterName) - var optionSpan = argument.AsSpan(1); - return new PowerShellArgument(PowerShellParsedType.Option) - { - Option = LegacyOptionName.MakeKebabCase(optionSpan, PowerShellStyleParser.ConvertPascalCaseToKebabCase), - }; - } - - // 处理各种类型的位置参数和选项值 - if (lastType is PowerShellParsedType.Start or PowerShellParsedType.CommandNameOrPositionalArgument) - { - // 如果是第一个参数,则后续可能是命令名或位置参数。 - // 如果可能是命令名或位置参数,则后续也可能是命令名或位置参数。 - var isValidName = LegacyOptionName.IsValidOptionName(argument.AsSpan()); - return new PowerShellArgument(isValidName ? PowerShellParsedType.CommandNameOrPositionalArgument : PowerShellParsedType.PositionalArgument) - { - Value = argument.AsSpan(), - }; - } - - if (lastType is PowerShellParsedType.PositionalArgument) - { - // 如果前一个是位置参数,则当前也是位置参数。 - return new PowerShellArgument(PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is PowerShellParsedType.Option) - { - // 如果前一个是选项,则当前是选项值。 - return new PowerShellArgument(PowerShellParsedType.OptionValue) { Value = argument.AsSpan() }; - } - - if (lastType is PowerShellParsedType.OptionValue) - { - // 如果前一个已经是选项值了,那么后一个是位置参数。 - return new PowerShellArgument(PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } - - if (lastType is PowerShellParsedType.PositionalArgumentSeparator or PowerShellParsedType.PostPositionalArgument) - { - // 如果前一个是位置参数分隔符或后置位置参数,则当前是后置位置参数。 - return new PowerShellArgument(PowerShellParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; - } - - // 其他情况,都视为位置参数。 - return new PowerShellArgument(PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan() }; - } -} - -internal enum PowerShellParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// PowerShell风格的选项。-ParameterName - /// - Option, - - /// - /// 选项值。 - /// - OptionValue, - - /// - /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 - /// - PositionalArgumentSeparator, - - /// - /// 后置的位置参数。 - /// - PostPositionalArgument, -} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs deleted file mode 100644 index 4a95dec9..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/UrlStyleParser.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Web; -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Collections; - -namespace DotNetCampus.Cli.Utils.Parsers; - -/// -/// URL风格的命令行参数解析器 -/// 用于解析来自Web的URL格式命令行参数 -/// -internal sealed class UrlStyleParser : ICommandLineParser -{ - internal static bool ConvertPascalCaseToKebabCase { get; } = false; - private const string FragmentName = "fragment"; - private readonly string _scheme; - - /// - /// 创建URL风格解析器 - /// - /// URL方案名(scheme) - public UrlStyleParser(string scheme) - { - _scheme = scheme; - } - - public LegacyCommandLineParsedResult Parse(IReadOnlyList commandLineArguments) - { - if (commandLineArguments.Count is not 1) - { - throw new CommandLineParseException($"URL style parser expects exactly one argument, but got {commandLineArguments.Count}."); - } - - var url = commandLineArguments[0]; - - var longOptions = new OptionDictionary(true); - var shortOptions = new OptionDictionary(true); - List possibleCommandNames = []; - List arguments = []; - - string? lastParameterName = null; - var lastType = UrlParsedType.Start; - - for (var i = 0; i < url.Length;) - { - var result = UrlPart.ReadNext(url, ref i, lastType); - lastType = result.Type; - - if (result.Type is UrlParsedType.CommandNameOrPositionalArgument) - { - lastParameterName = null; - var commandNameOrPositionalArgument = result.Value; - possibleCommandNames.Add(commandNameOrPositionalArgument); - arguments.Add(commandNameOrPositionalArgument); - continue; - } - - if (result.Type is UrlParsedType.PositionalArgument) - { - lastParameterName = null; - arguments.Add(result.Value); - continue; - } - - if (result.Type is UrlParsedType.ParameterName) - { - lastParameterName = result.Name; - longOptions.AddOption(result.Name); - continue; - } - - if (result.Type is UrlParsedType.ParameterValue) - { - if (lastParameterName is null) - { - throw new CommandLineParseException($"Invalid URL format: {url}. Parameter value '{result.Value}' without a name."); - } - - longOptions.AddValue(lastParameterName, result.Value); - lastParameterName = null; - continue; - } - - if (result.Type is UrlParsedType.Fragment) - { - lastParameterName = null; - longOptions.AddValue(result.Name, result.Value); - continue; - } - } - - return new LegacyCommandLineParsedResult( - LegacyCommandLineParsedResult.MakePossibleCommandNames(possibleCommandNames, ConvertPascalCaseToKebabCase), - longOptions, - shortOptions, - arguments.ToReadOnlyList()); - } - - - internal readonly ref struct UrlPart(UrlParsedType type) - { - public UrlParsedType Type { get; } = type; - public string Name { get; private init; } = ""; - public string Value { get; private init; } = ""; - - public static UrlPart ReadNext(string url, ref int index, UrlParsedType lastType) - { - if (lastType is UrlParsedType.Start) - { - // 取出第一个位置参数(或命令名) - var startIndex = -1; - for (var i = index; i < url.Length - 3; i++) - { - if (url[i] is ':' && url[i + 1] is '/' && url[i + 2] is '/') - { - startIndex = i + 3; - break; - } - } - if (startIndex < 0) - { - // 如果是开始,则必须包含 scheme:// - throw new CommandLineParseException($"Invalid URL format: {url}. Missing '://'"); - } - var endIndex = url.IndexOfAny(['/', '?', '#', '&'], startIndex); - if (endIndex < 0) - { - // 此 URL 没有选项,最后一个值是位置参数或命令名 - endIndex = url.Length; - index = endIndex + 1; - } - else - { - index = endIndex; - } - - var value = HttpUtility.UrlDecode(url.AsSpan(startIndex, endIndex - startIndex).ToString()); - var isValidName = LegacyOptionName.IsValidOptionName(value.AsSpan()); - return new UrlPart(isValidName ? UrlParsedType.CommandNameOrPositionalArgument : UrlParsedType.PositionalArgument) - { - Value = value, - }; - } - - if (lastType is UrlParsedType.CommandNameOrPositionalArgument) - { - return url[index] switch - { - // 取出非第一个位置参数(或命令名) - '/' => ReadNextPositionalArgument(url, ref index, lastType), - // 查询参数名。 - '?' => ReadNextParameterName(url, ref index), - // 片段。 - '#' => ReadFragment(url, ref index), - _ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '/', '?' or '#' after a positional argument."), - }; - } - - if (lastType is UrlParsedType.PositionalArgument) - { - return url[index] switch - { - // 新的位置参数。 - '/' => ReadNextPositionalArgument(url, ref index, lastType), - // 查询参数名。 - '?' => ReadNextParameterName(url, ref index), - // 片段。 - '#' => ReadFragment(url, ref index), - _ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '/', '?' or '#' after a positional argument."), - }; - } - - if (lastType is UrlParsedType.ParameterName) - { - return url[index] switch - { - // 查询参数值。 - '=' => ReadNextParameterValue(url, ref index), - // 查询新的参数名。 - '&' => ReadNextParameterName(url, ref index), - // 片段。 - '#' => ReadFragment(url, ref index), - _ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '=', '&' or '#' after a parameter name."), - }; - } - - if (lastType is UrlParsedType.ParameterValue) - { - return url[index] switch - { - // 查询新的参数名。 - '&' => ReadNextParameterName(url, ref index), - // 片段。 - '#' => ReadFragment(url, ref index), - _ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '&' or '#' after a parameter value."), - }; - } - - throw new CommandLineParseException($"Invalid URL format: {url}"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UrlPart ReadNextPositionalArgument(string url, ref int index, UrlParsedType lastType) - { - var startIndex = index; - var endIndex = url.IndexOfAny(['/', '?', '#', '&'], startIndex + 1); - if (endIndex < 0) - { - endIndex = url.Length; - index = endIndex + 1; - } - else - { - index = endIndex; - } - var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString()); - var isValidName = LegacyOptionName.IsValidOptionName(value.AsSpan()); - var type = lastType is UrlParsedType.PositionalArgument - ? UrlParsedType.PositionalArgument - : UrlParsedType.CommandNameOrPositionalArgument; - index = endIndex; - return new UrlPart(isValidName ? type : UrlParsedType.PositionalArgument) - { - Value = value, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UrlPart ReadNextParameterName(string url, ref int index) - { - var startIndex = index; - var endIndex = url.IndexOfAny(['=', '#', '&'], index + 1); - if (endIndex < 0) - { - endIndex = url.Length; - index = endIndex + 1; - } - else - { - index = endIndex; - } - var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString()); - index = endIndex; - return new UrlPart(UrlParsedType.ParameterName) - { - Name = LegacyOptionName.MakeKebabCase(value -#if !NETCOREAPP3_1_OR_GREATER - .AsSpan() -#endif - ), - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UrlPart ReadNextParameterValue(string url, ref int index) - { - var startIndex = index; - var endIndex = url.IndexOfAny(['&', '#'], index + 1); - if (endIndex < 0) - { - endIndex = url.Length; - index = endIndex + 1; - } - else - { - index = endIndex; - } - var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString()); - index = endIndex; - return new UrlPart(UrlParsedType.ParameterValue) - { - Value = value, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UrlPart ReadFragment(string url, ref int index) - { - var startIndex = index; - index = url.Length + 1; - return new UrlPart(UrlParsedType.Fragment) - { - Name = FragmentName, - Value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1).ToString()), - }; - } - } -} - -internal enum UrlParsedType -{ - /// - /// 尚未开始解析。 - /// - Start, - - /// - /// 前几个位置参数,也可能是命令名。 - /// - CommandNameOrPositionalArgument, - - /// - /// 位置参数。 - /// - PositionalArgument, - - /// - /// 查询参数名。 - /// - ParameterName, - - /// - /// 查询参数值。 - /// - ParameterValue, - - /// - /// 片段参数名。 - /// - Fragment, -} From e22e027c4245823d043e44f81344fd0872dc38a4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 13:49:33 +0800 Subject: [PATCH 063/193] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E7=9A=84=204.0=20=E7=9A=84=E4=B8=A4=E6=AD=A5=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModelProviding/CommandModelProvider.cs | 27 --- ...ommandHandlersFromThisAssemblyAttribute.cs | 9 - .../Compiler/RawArgumentsAttribute.cs | 6 + .../Utils/Collections/ReadOnlyListRange.cs | 82 --------- .../Utils/Collections/SingleOptimizedList.cs | 165 ------------------ .../Utils/Handlers/TaskCommandHandler.cs | 18 -- .../Utils/Parsers/CommandLineParser.cs | 44 ++--- .../Utils/Parsers/OptionName.cs | 19 -- .../Fakes/AssemblyCommandHandler.cs | 6 - 9 files changed, 28 insertions(+), 348 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine/Compiler/CollectCommandHandlersFromThisAssemblyAttribute.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Collections/ReadOnlyListRange.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Collections/SingleOptimizedList.cs delete mode 100644 src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/AssemblyCommandHandler.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index c4e882f0..af33093b 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -100,33 +100,6 @@ public static IncrementalValuesProvider SelectComm .Where(m => m is not null) .Select((m, ct) => m!); } - - public static IncrementalValuesProvider SelectAssemblyCommands(this IncrementalGeneratorInitializationContext context) - { - return context.SyntaxProvider.ForAttributeWithMetadataName(typeof(CollectCommandHandlersFromThisAssemblyAttribute).FullName!, (node, ct) => - { - if (node is not ClassDeclarationSyntax cds) - { - // 必须是类型。 - return false; - } - - return true; - }, (c, ct) => - { - var typeSymbol = c.TargetSymbol; - var rootNamespace = typeSymbol.ContainingNamespace.ToDisplayString(); - var typeName = typeSymbol.Name; - var attribute = typeSymbol.GetAttributes() - .FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); - - return new AssemblyCommandsGeneratingModel - { - Namespace = rootNamespace, - AssemblyCommandHandlerType = (INamedTypeSymbol)typeSymbol, - }; - }); - } } file static class Extensions diff --git a/src/DotNetCampus.CommandLine/Compiler/CollectCommandHandlersFromThisAssemblyAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/CollectCommandHandlersFromThisAssemblyAttribute.cs deleted file mode 100644 index 26c98482..00000000 --- a/src/DotNetCampus.CommandLine/Compiler/CollectCommandHandlersFromThisAssemblyAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace DotNetCampus.Cli.Compiler; - -/// -/// 在一个 partial 类上标记,源生成器会自动查找此类型所在项目中所有支持的命令,并允许添加到 命令行解析器中执行。 -/// -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public class CollectCommandHandlersFromThisAssemblyAttribute : Attribute -{ -} diff --git a/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs index c816369a..d38aefc6 100644 --- a/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs +++ b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs @@ -9,4 +9,10 @@ namespace DotNetCampus.Cli.Compiler; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class RawArgumentsAttribute : CommandLineAttribute { + /// + /// 设置为 时,如果传入 URL,则会自动将其转换为普通的命令行参数列表(选项自动添加 -- 前缀,但不会改变命名规则)。
+ /// 设置为 时,则不会转换 URL,Main 方法传入时收到什么就是什么。
+ /// 默认值为 。 + ///
+ public bool ConvertUrl { get; set; } = true; } diff --git a/src/DotNetCampus.CommandLine/Utils/Collections/ReadOnlyListRange.cs b/src/DotNetCampus.CommandLine/Utils/Collections/ReadOnlyListRange.cs deleted file mode 100644 index 9965ff50..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Collections/ReadOnlyListRange.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections; - -namespace DotNetCampus.Cli.Utils.Collections; - -/// -/// 从一个只读集合中取出一个范围,让此集合表现得就像那个范围内的一个子集合一样。 -/// -/// 集合的元素类型。 -internal readonly struct ReadOnlyListRange : IReadOnlyList -{ - private readonly IReadOnlyList? _sourceList; - private readonly Range _range; - - /// - /// 从一个只读集合中取出一个范围,让此集合表现得就像那个范围内的一个子集合一样。 - /// - /// 原集合。 - /// 范围。 - public ReadOnlyListRange(IReadOnlyList sourceList, Range range) - { - _sourceList = sourceList; - _range = range; - } - - public int Count => _range.GetOffsetAndLength(_sourceList?.Count ?? 0).Length; - - public T this[int index] => _sourceList is null - ? throw new ArgumentOutOfRangeException(nameof(index)) - : _sourceList[_range.GetOffsetAndLength(_sourceList.Count).Offset + index]; - - /// - /// 获取第一个元素,如果没有元素则返回默认值。 - /// - public T? FirstOrDefault => Count is 0 ? default : this[0]; - - public ReadOnlyListRange Slice(int offset, int length) - { - if (_sourceList is null) - { - return offset is 0 && length is 0 - ? new ReadOnlyListRange([], new Range(0, 0)) - : throw new ArgumentOutOfRangeException(nameof(length)); - } - - var (start, _) = _range.GetOffsetAndLength(_sourceList.Count); - return new ReadOnlyListRange(_sourceList, new Range(start + offset, start + offset + length)); - } - - public IEnumerator GetEnumerator() - { - if (_sourceList is null) - { - yield break; - } - - var (offset, length) = _range.GetOffsetAndLength(_sourceList.Count); - for (var i = offset; i < offset + length; i++) - { - yield return _sourceList[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} - -internal static class ReadOnlyListRangeExtensions -{ - public static ReadOnlyListRange Slice(this IReadOnlyList sourceList, Range range) - { - return new ReadOnlyListRange(sourceList, range); - } - - public static ReadOnlyListRange Slice(this IReadOnlyList sourceList, int offset, int length) - { - return new ReadOnlyListRange(sourceList, new Range(offset, offset + length)); - } - - public static ReadOnlyListRange ToReadOnlyList(this IReadOnlyList sourceList) - { - return new ReadOnlyListRange(sourceList, new Range(0, sourceList.Count)); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Collections/SingleOptimizedList.cs b/src/DotNetCampus.CommandLine/Utils/Collections/SingleOptimizedList.cs deleted file mode 100644 index b2c55a0a..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Collections/SingleOptimizedList.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; - -namespace DotNetCampus.Cli.Utils.Collections; - -/// -/// 为 0 个和 1 个值特殊优化的列表。 -/// -[DebuggerDisplay(nameof(SingleOptimizedList) + " {_firstValue,nq}, {_restValues}")] -internal readonly struct SingleOptimizedList : IReadOnlyList -{ - /// - /// 是否有值。如果为 ,则是空列表。 - /// - [MemberNotNullWhen(true, nameof(_firstValue))] - private bool HasValue { get; } - - /// - /// 在此命令行解析的上下文中,通常也不会为空字符串或空白字符串。 - /// - private readonly T? _firstValue; - - /// - /// 当所需储存的值超过 1 个时,将启用此列表。所以此列表要么为 null,要么有多于 1 个的值。 - /// - private readonly List? _restValues; - - public SingleOptimizedList() - { - } - - public SingleOptimizedList(T value) - { - HasValue = true; - _firstValue = value; - } - - private SingleOptimizedList(T firstValue, List restValues) - { - HasValue = true; - _firstValue = firstValue; - _restValues = restValues; - } - - /// - /// 获取集合中值的个数。 - /// - public int Count => HasValue switch - { - false => 0, - true when _restValues is null => 1, - true => _restValues.Count + 1, - }; - - /// - /// 获取集合中指定索引处的值。 - /// - public T this[int index] => HasValue - ? index is 0 ? _firstValue! : _restValues![index - 1] - : throw new ArgumentOutOfRangeException(nameof(index), "集合中没有值。"); - - /// - /// 添加一个值到集合中,并返回包含该值的新集合。 - /// - /// 要添加的值。 - [Pure] - public SingleOptimizedList Add(T value) - { - if (!HasValue) - { - // 空集合,添加第一个值。 - return new SingleOptimizedList(value); - } - - if (_restValues is null) - { - // 只有一个值,添加第二个值。 - return new SingleOptimizedList(_firstValue, [value]); - } - - // 已经有多个值,添加到现有的列表中。 - // 注意!此行为与其他任何集合都不同,会导致新旧对象共享同一个列表的引用,同时被修改!所以日常不要使用此集合。 - _restValues.Add(value); - return new SingleOptimizedList(_firstValue, _restValues); - } - - public SingleOptimizedList AddRange(IReadOnlyList values) - { - if (values.Count is 0) - { - return this; - } - - if (values.Count is 1) - { - return Add(values[0]); - } - - if (!HasValue) - { - // 空集合,添加第一个值。 - return new SingleOptimizedList(values[0], values.Skip(1).ToList()); - } - - if (_restValues is null) - { - // 只有一个值,添加第二个值。 - return new SingleOptimizedList(_firstValue, values.ToList()); - } - - // 已经有多个值,添加到现有的列表中。 - // 注意!此行为与其他任何集合都不同,会导致新旧对象共享同一个列表的引用,同时被修改!所以日常不要使用此集合。 - _restValues.AddRange(values); - return new SingleOptimizedList(_firstValue, _restValues); - } - - public IEnumerator GetEnumerator() - { - if (!HasValue) - { - yield break; - } - - yield return _firstValue; - - if (_restValues is not { } restValues) - { - yield break; - } - - for (var i = 0; i < restValues.Count; i++) - { - var value = restValues[i]; - yield return value; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} - -internal static class SingleOptimizedListExtensions -{ - public static bool TryAdd(this Dictionary> dictionary, TKey key, TValue value) - where TKey : notnull - { - if (dictionary.TryGetValue(key, out var list)) - { - // 已经有值了,添加到列表中。 - dictionary[key] = list.Add(value); - return false; - } - - // 没有值,添加一个新的值。 - dictionary[key] = new SingleOptimizedList(value); - return true; - } - - public static void AddOrUpdateSingle(this Dictionary> dictionary, TKey key, TValue value) - where TKey : notnull - { - dictionary[key] = new SingleOptimizedList(value); - } -} diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 84ba9e84..91b2531f 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -2,24 +2,6 @@ namespace DotNetCampus.Cli.Utils.Handlers; -internal sealed class TaskCommandHandler( - Func optionsCreator, - Func> handler) : ICommandHandler - where TOptions : class -{ - private TOptions? _options; - - public Task RunAsync() - { - _options ??= optionsCreator(); - if (_options is null) - { - throw new InvalidOperationException($"No options of type {typeof(TOptions)} were created."); - } - return handler(_options); - } -} - internal sealed class AnonymousCommandHandler( CommandLine commandLine, CommandObjectFactory factory, diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 229c9f5e..00152121 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -124,18 +124,18 @@ public CommandLineParsingResult Parse() // 如果当前是一个选项,则记录下来,供后面解析选项值时使用。 var optionMatch = state switch { - Cat.LongOption => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), - Cat.ShortOption => MatchShortOption(optionName.Name, _caseSensitive), - _ => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy) switch + Cat.LongOption => MatchLongOption(optionName, _caseSensitive, _namingPolicy), + Cat.ShortOption => MatchShortOption(optionName, _caseSensitive), + _ => MatchLongOption(optionName, _caseSensitive, _namingPolicy) switch { - { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName.Name, _caseSensitive), + { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName, _caseSensitive), var t => t, }, }; if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName.Name); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName); } if (optionMatch.ValueType is OptionValueType.Boolean) { @@ -172,18 +172,18 @@ public CommandLineParsingResult Parse() { var optionMatch = state switch { - Cat.LongOptionWithValue => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy), - Cat.ShortOptionWithValue => MatchShortOption(optionName.Name, _caseSensitive), - _ => MatchLongOption(optionName.Name, _caseSensitive, _namingPolicy) switch + Cat.LongOptionWithValue => MatchLongOption(optionName, _caseSensitive, _namingPolicy), + Cat.ShortOptionWithValue => MatchShortOption(optionName, _caseSensitive), + _ => MatchLongOption(optionName, _caseSensitive, _namingPolicy) switch { - { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName.Name, _caseSensitive), + { ValueType: OptionValueType.NotExist } => MatchShortOption(optionName, _caseSensitive), var t => t, }, }; if (optionMatch.ValueType is OptionValueType.NotExist) { // 如果选项不存在,则报告错误。 - return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName.Name); + return CommandLineParsingResult.OptionalArgumentNotFound(_commandLine, index, _commandObjectName, optionName); } result = AssignOptionValue(optionMatch, value).Combine(result); break; @@ -195,7 +195,7 @@ public CommandLineParsingResult Parse() } case Cat.MultiShortOptions: { - var name = optionName.Name; + var name = optionName; if (SupportsMultiCharShortOption) { // 如果支持多字符短选项,则优先作为多字符短选项处理。 @@ -211,9 +211,9 @@ public CommandLineParsingResult Parse() if (SupportsShortOptionCombination) { // 如果不支持,则尝试逐个处理多个短选项。 - for (var i = 0; i < optionName.Name.Length; i++) + for (var i = 0; i < optionName.Length; i++) { - var n = optionName.Name[i..(i + 1)]; + var n = optionName[i..(i + 1)]; var optionMatch = MatchShortOption(n, _caseSensitive); if (optionMatch.ValueType is OptionValueType.NotExist) { @@ -425,7 +425,7 @@ public CommandArgumentPart(CommandLineParser parser, string argument, Cat lastTy /// /// 如果此参数是一个选项(长选项或短选项),则为此选项的名称;否则为默认值。 /// - public OptionName Option { get; private set; } + public ReadOnlySpan Option { get; private set; } /// /// 如果此参数包含值(位置参数或选项值),则为此值;否则为默认值。 @@ -438,7 +438,7 @@ public CommandArgumentPart(CommandLineParser parser, string argument, Cat lastTy /// 此参数的类型。 /// 如果此参数是一个选项(长选项或短选项),则为此选项的名称;否则为默认值。 /// 如果此参数包含值(位置参数或选项值),则为此值;否则为默认值。 - public void Deconstruct(out Cat type, out OptionName optionName, out ReadOnlySpan value) + public void Deconstruct(out Cat type, out ReadOnlySpan optionName, out ReadOnlySpan value) { type = Type; optionName = Option; @@ -606,13 +606,13 @@ private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) { // 带值的长选项。 Type = Cat.LongOptionWithValue; - Option = new OptionName(true, argument[..index]); + Option = argument[..index]; Value = argument[(index + 1)..]; return true; } // 不带值的长选项。 Type = Cat.LongOption; - Option = new OptionName(true, argument); + Option = argument; return true; } @@ -633,7 +633,7 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) { // 单独的短选项。 Type = Cat.ShortOption; - Option = new OptionName(false, argument); + Option = argument; return true; } if (index > 0) @@ -642,7 +642,7 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) { // 带值的短选项。 Type = Cat.ShortOptionWithValue; - Option = new OptionName(false, argument[..index]); + Option = argument[..index]; Value = argument[(index + 1)..]; return true; } @@ -653,7 +653,7 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) // 直接返回,延迟处理。 Type = Cat.MultiShortOptions; - Option = new OptionName(false, argument); + Option = argument; return true; } @@ -674,13 +674,13 @@ private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan a { // 带值的选项。 Type = Cat.OptionWithValue; - Option = new OptionName(true, argument[..index]); + Option = argument[..index]; Value = argument[(index + 1)..]; return true; } // 不带值的选项。 Type = Cat.Option; - Option = new OptionName(true, argument); + Option = argument; return true; } diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs deleted file mode 100644 index 145321d4..00000000 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/OptionName.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DotNetCampus.Cli.Utils.Parsers; - -internal readonly ref struct OptionName(bool isLongOption, ReadOnlySpan optionName) -{ - /// - /// 表示长选项, 表示短选项。 - /// - internal bool IsLongOption { get; } = isLongOption; - - /// - /// 选项名称,不包含前缀符号。 - /// - internal ReadOnlySpan Name { get; } = optionName; - - public override string ToString() - { - return Name.ToString(); - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/AssemblyCommandHandler.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/AssemblyCommandHandler.cs deleted file mode 100644 index 0fab08b8..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/AssemblyCommandHandler.cs +++ /dev/null @@ -1,6 +0,0 @@ -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; From 684f4f3b4e32356b479b9655463b66915ad6ea67 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 13:51:56 +0800 Subject: [PATCH 064/193] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E7=9A=84=204.0=20=E7=9A=84=E4=B8=A4=E6=AD=A5=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 1 + .../CommandLinePropertyValue.cs | 616 ------------------ .../{ => Utils}/CommandSeparatorChars.cs | 2 +- 3 files changed, 2 insertions(+), 617 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine/CommandLinePropertyValue.cs rename src/DotNetCampus.CommandLine/{ => Utils}/CommandSeparatorChars.cs (98%) diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 4f43a17f..fce5a970 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.Contracts; using DotNetCampus.Cli.Utils; +using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; diff --git a/src/DotNetCampus.CommandLine/CommandLinePropertyValue.cs b/src/DotNetCampus.CommandLine/CommandLinePropertyValue.cs deleted file mode 100644 index 4f72b011..00000000 --- a/src/DotNetCampus.CommandLine/CommandLinePropertyValue.cs +++ /dev/null @@ -1,616 +0,0 @@ -using System.Collections; -using System.Collections.ObjectModel; -using DotNetCampus.Cli.Exceptions; -#if NETCOREAPP3_1_OR_GREATER -using System.Collections.Immutable; -#endif - -namespace DotNetCampus.Cli; - -/// -/// 包含从命令行解析出来的属性值,可供转换为各种常见类型。 -/// -public readonly struct CommandLinePropertyValue : IReadOnlyList -{ - private readonly IReadOnlyList? _values; - private readonly MultiValueHandling _multiValueHandling; - - internal CommandLinePropertyValue(IReadOnlyList values, MultiValueHandling multiValueHandling) - { - _values = values; - _multiValueHandling = multiValueHandling; - } - - IEnumerator IEnumerable.GetEnumerator() => (_values ?? []).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => (_values ?? []).GetEnumerator(); - int IReadOnlyCollection.Count => _values?.Count ?? 0; - string IReadOnlyList.this[int index] => _values?[index] ?? throw new IndexOutOfRangeException(); - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator bool(CommandLinePropertyValue propertyValue) - { - return propertyValue._values switch - { - // 没传选项时,相当于传了 false。 - null => false, - // 传了选项时,相当于传了 true。 - { Count: 0 } => true, - // 传了选项,后面还带了参数时,取第一个参数的值作为 true/false。 - { } values => ParseBoolean(values[0]) ?? throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid boolean value. Available values are: 1, true, yes, on, 0, false, no, off."), - }; - - static bool? ParseBoolean(string value) - { - var isTrue = value.Equals("1", StringComparison.OrdinalIgnoreCase) || - value.Equals("true", StringComparison.OrdinalIgnoreCase) || - value.Equals("yes", StringComparison.OrdinalIgnoreCase) || - value.Equals("on", StringComparison.OrdinalIgnoreCase); - if (isTrue) - { - return true; - } - var isFalse = value.Equals("0", StringComparison.OrdinalIgnoreCase) || - value.Equals("false", StringComparison.OrdinalIgnoreCase) || - value.Equals("no", StringComparison.OrdinalIgnoreCase) || - value.Equals("off", StringComparison.OrdinalIgnoreCase); - if (isFalse) - { - return false; - } - return null; - } - } - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator byte(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => byte.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid byte value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator sbyte(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => sbyte.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid sbyte value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator char(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => char.TryParse(values[0], out var result) ? result : '\0', - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator decimal(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => decimal.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid decimal value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator double(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => double.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid double value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator float(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => float.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid float value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator int(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => int.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid int value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator uint(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => uint.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid uint value."), - }; - -#if NET5_0_OR_GREATER - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator nint(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => nint.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid nint value."), - }; -#endif - -#if NET5_0_OR_GREATER - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator nuint(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => nuint.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid unint value."), - }; -#endif - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator long(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => long.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid long value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator ulong(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => ulong.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid ulong value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator short(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => short.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid short value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator ushort(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => default, - { } values => ushort.TryParse(values[0], out var result) - ? result - : throw new CommandLineParseValueException( - $"Value [{values[0]}] is not a valid ushort value."), - }; - - /// - /// 将从命令行解析出来的属性值转换为 。 - /// - public static implicit operator string(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => "", - { } values => propertyValue._multiValueHandling switch - { - MultiValueHandling.First => values[0], - MultiValueHandling.Last => values[^1], -#if NETCOREAPP3_1_OR_GREATER - MultiValueHandling.SpaceAll => string.Join(' ', values), - MultiValueHandling.SlashAll => string.Join('/', values), -#else - MultiValueHandling.SpaceAll => string.Join(" ", values), - MultiValueHandling.SlashAll => string.Join("/", values), -#endif - _ => values[0], - }, - }; - - /// - /// 将从命令行解析出来的属性值转换为字符串数组。 - /// - public static implicit operator string[](CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => [], - { } values => [..SplitValues(values)], - }; - -#if NETCOREAPP3_1_OR_GREATER - /// - /// 将从命令行解析出来的属性值转换为不可变字符串数组。 - /// - public static implicit operator ImmutableArray(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { -#if NET8_0_OR_GREATER - null or { Count: 0 } => [], - { } values => [..SplitValues(values)], -#else - null or { Count: 0 } => ImmutableArray.Empty, - { } values => SplitValues(values).ToImmutableArray(), -#endif - }; -#endif - -#if NETCOREAPP3_1_OR_GREATER - /// - /// 将从命令行解析出来的属性值转换为不可变字符串哈希集合。 - /// - public static implicit operator ImmutableHashSet(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { -#if NET8_0_OR_GREATER - null or { Count: 0 } => [], - { } values => [..SplitValues(values)], -#else - null or { Count: 0 } => ImmutableHashSet.Empty, - { } values => SplitValues(values).ToImmutableHashSet(), -#endif - }; -#endif - - /// - /// 将从命令行解析出来的属性值转换为字符串集合。 - /// - public static implicit operator Collection(CommandLinePropertyValue propertyValue) => propertyValue._values switch - { - null or { Count: 0 } => [], - { } values => [..SplitValues(values)], - }; - - /// - /// 将从命令行解析出来的属性值转换为字符串列表。 - /// - public static implicit operator List(CommandLinePropertyValue propertyValue) => propertyValue.ToList(); - - /// - /// 将从命令行解析出来的属性值转换为字符串键值对。 - /// - public static implicit operator KeyValuePair(CommandLinePropertyValue propertyValue) => propertyValue.ToDictionary().FirstOrDefault(); - - /// - /// 将从命令行解析出来的属性值转换为字符串字典。 - /// - public static implicit operator Dictionary(CommandLinePropertyValue propertyValue) => propertyValue.ToDictionary(); - - /// - /// 将从命令行解析出来的属性值转换为枚举值。 - /// - public T ToEnum() where T : unmanaged, Enum => _values switch - { - null or { Count: 0 } => default, - { } values => Enum.TryParse(values[0], true, out var result) ? result : default!, - }; - - /// - /// 将从命令行解析出来的属性值转换为字符串列表。 - /// - public List ToList() => _values switch - { - null or { Count: 0 } => [], - { } values => [..SplitValues(values)], - }; - - /// - /// 将从命令行解析出来的属性值转换为字符串字典。 - /// - public Dictionary ToDictionary() => _values switch - { - null or { Count: 0 } => new Dictionary(), - { } values => values - .SelectMany(x => x.Split( -#if NETCOREAPP3_1_OR_GREATER - ';' -#else - [";"] -#endif - , StringSplitOptions.RemoveEmptyEntries)) - .Select(x => - { - var parts = x.Split('='); - if (parts.Length is not 2) - { - throw new CommandLineParseValueException( - $"Value [{x}] is not a valid dictionary. Expected format is key1=value1;key2=value2."); - } - return new KeyValuePair(parts[0], parts[1]); - }) - .GroupBy(x => x.Key) - .ToDictionary(x => x.Key, x => x.Last().Value), - }; - - private static IEnumerable SplitValues(IReadOnlyList commandLineValues) - { - for (var commandLineValueIndex = 0; commandLineValueIndex < commandLineValues.Count; commandLineValueIndex++) - { - var optionValue = commandLineValues[commandLineValueIndex]; - var lastPart = ListValueParsingType.Start; - var thisPartStartIndex = 0; - for (var index = 0; index < optionValue.Length; index++) - { - var c = optionValue[index]; - - // 引号 - if (c is '"') - { - if (lastPart is ListValueParsingType.Start) - { - // 开始的引号 - lastPart = ListValueParsingType.QuoteStart; - continue; - } - if (lastPart is ListValueParsingType.QuoteStart) - { - // 连续出现的引号 - yield return ""; - lastPart = ListValueParsingType.QuoteEnd; - continue; - } - if (lastPart is ListValueParsingType.QuotedValue) - { - // 引号中值后的引号 - yield return optionValue[thisPartStartIndex..index]; - lastPart = ListValueParsingType.QuoteEnd; - continue; - } - if (lastPart is ListValueParsingType.QuotedSeparator) - { - // 引号中分割符后的引号 - yield return ""; - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuoteEnd) - { - // 引号结束后的引号 - lastPart = ListValueParsingType.QuoteStart; - continue; - } - if (lastPart is ListValueParsingType.Value) - { - // 正常值后的引号 - throw new CommandLineParseValueException( - $"Invalid value format at index [{index}]: {optionValue}"); - } - if (lastPart is ListValueParsingType.Separator) - { - // 正常分隔符后的引号 - lastPart = ListValueParsingType.QuoteStart; - continue; - } - } - - // 分割符 - if (c is ';' or ',') - { - if (lastPart is ListValueParsingType.Start) - { - // 开始的分割符 - yield return ""; - lastPart = ListValueParsingType.Separator; - continue; - } - if (lastPart is ListValueParsingType.QuoteStart) - { - // 引号后紧跟着的分割符(等同于正常字符) - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuotedValue) - { - // 引号中值后的分割符(等同于正常字符) - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuotedSeparator) - { - // 引号中连续出现的分割符(等同于正常字符) - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuoteEnd) - { - // 引号结束后的分割符 - lastPart = ListValueParsingType.Separator; - continue; - } - if (lastPart is ListValueParsingType.Value) - { - // 正常值后的分割符 - yield return optionValue[thisPartStartIndex..index]; - lastPart = ListValueParsingType.Separator; - continue; - } - if (lastPart is ListValueParsingType.Separator) - { - // 连续出现的分割符 - yield return ""; - lastPart = ListValueParsingType.Separator; - continue; - } - } - - // 其他字符 - if (lastPart is ListValueParsingType.Start) - { - // 开始的值 - thisPartStartIndex = index; - lastPart = ListValueParsingType.Value; - continue; - } - if (lastPart is ListValueParsingType.QuoteStart) - { - // 引号后紧跟着的值 - thisPartStartIndex = index; - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuotedValue) - { - // 引号中值后的值 - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuotedSeparator) - { - // 引号中分割符(实际上就是正常值)后的值 - lastPart = ListValueParsingType.QuotedValue; - continue; - } - if (lastPart is ListValueParsingType.QuoteEnd) - { - // 引号结束后的值 - throw new CommandLineParseValueException( - $"Invalid value format at index [{index}]: {optionValue}"); - } - if (lastPart is ListValueParsingType.Value) - { - // 正常值后的值 - lastPart = ListValueParsingType.Value; - continue; - } - if (lastPart is ListValueParsingType.Separator) - { - // 正常分割符后的值 - thisPartStartIndex = index; - lastPart = ListValueParsingType.Value; - continue; - } - } - - // 处理最后一个值 - if (lastPart is ListValueParsingType.Start) - { - // 一开始就结束了(字符串里就没有值) - yield return ""; - } - else if (lastPart is ListValueParsingType.QuoteStart or ListValueParsingType.QuotedValue or ListValueParsingType.QuotedSeparator) - { - // 引号还没结束,字符串就结束了 - throw new CommandLineParseValueException( - $"Missing quote end at index [{optionValue.Length}]: {optionValue}"); - } - else if (lastPart is ListValueParsingType.QuoteEnd) - { - // 引号结束后字符串正常结束 - } - else if (lastPart is ListValueParsingType.Value) - { - // 正常值结束的字符串 - yield return optionValue[thisPartStartIndex..]; - } - else if (lastPart is ListValueParsingType.Separator) - { - // 正常分割符后就结束了字符串 - yield return ""; - } - } - } -} - -file enum ListValueParsingType -{ - /// - /// 尚未开始分割。 - /// - Start, - - /// - /// 引号开始。 - /// - QuoteStart, - - /// - /// 引号中的值。 - /// - QuotedValue, - - /// - /// 引号中的分割符。 - /// - QuotedSeparator, - - /// - /// 引号结束。 - /// - QuoteEnd, - - /// - /// 正常值。 - /// - Value, - - /// - /// 正常分割符。 - /// - Separator, -} - -internal enum MultiValueHandling -{ - /// - /// 仅返回第一个值。 - /// - First, - - /// - /// 返回最后一个值。 - /// - Last, - - /// - /// 用空格连接返回所有值。 - /// - SpaceAll, - - /// - /// 用斜杠 '/' 连接返回所有值。 - /// - SlashAll, -} diff --git a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs similarity index 98% rename from src/DotNetCampus.CommandLine/CommandSeparatorChars.cs rename to src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs index ff2b6ad4..aafefbbe 100644 --- a/src/DotNetCampus.CommandLine/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Runtime.CompilerServices; -namespace DotNetCampus.Cli; +namespace DotNetCampus.Cli.Utils; /// /// 允许用户在命令行中使用的分隔符字符集合。最多只能支持 个字符。 From 70b2be4036ecaeacf0ae15ba5bacf4a1733a864e Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 14:11:36 +0800 Subject: [PATCH 065/193] =?UTF-8?q?=E6=9B=B4=E4=B8=A5=E6=A0=BC=E5=9C=B0?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=20option=20=E7=9A=84=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 6 +--- .../OptionLongNameMustBeKebabCaseAnalyzer.cs | 34 ++++++++----------- .../CommandLineParsingOptions.cs | 1 - 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index 57ba24c2..743120fb 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -51,12 +51,8 @@ public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer /// public override void Initialize(AnalysisContext context) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs index 6d313e9d..5d2d84ac 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs @@ -39,12 +39,8 @@ public class OptionLongNameMustBeKebabCaseAnalyzer : DiagnosticAnalyzer /// public override void Initialize(AnalysisContext context) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration); } @@ -115,26 +111,26 @@ private void AnalyzeAttribute(SyntaxNodeAnalysisContext context, SyntaxList().ToList(); var longNameExpression = attributeArguments.FirstOrDefault()?.Expression as LiteralExpressionSyntax; var longName = longNameExpression?.Token.ValueText; - var ignoreCaseExpression = - attributeArguments.FirstOrDefault(x => x.NameEquals?.Name.ToString() == "ExactSpelling")?.Expression as LiteralExpressionSyntax; - var exactSpelling = ignoreCaseExpression?.Token.ValueText.Equals("true", StringComparison.OrdinalIgnoreCase) is true; - if (!exactSpelling && longName is not null) + var caseSensitiveExpression = + attributeArguments.FirstOrDefault(x => x.NameEquals?.Name.ToString() == "CaseSensitive")?.Expression as LiteralExpressionSyntax; + var caseSensitive = caseSensitiveExpression?.Token.ValueText.Equals("true", StringComparison.OrdinalIgnoreCase) is true; + if (!caseSensitive && longName is not null) { // 严格检查。 - var kebabCase1 = MakeKebabCase(longName, true, false, hasSeparator); - var isKebabCase1 = string.Equals(kebabCase1, longName, StringComparison.Ordinal); - if (!isKebabCase1) + var kebabCase = MakeKebabCase(longName, true, true, hasSeparator); + var isKebabCase = string.Equals(kebabCase, longName, StringComparison.Ordinal); + if (!isKebabCase) { return (longName, longNameExpression?.GetLocation(), SuggestionType.Warning); } - // 宽松检查。 - var kebabCase2 = MakeKebabCase(longName, true, true, hasSeparator); - var isKebabCase2 = string.Equals(kebabCase2, longName, StringComparison.Ordinal); - if (!isKebabCase2) - { - return (longName, longNameExpression?.GetLocation(), SuggestionType.Hidden); - } + // // 宽松检查。 + // var kebabCase2 = MakeKebabCase(longName, true, true, hasSeparator); + // var isKebabCase2 = string.Equals(kebabCase2, longName, StringComparison.Ordinal); + // if (!isKebabCase2) + // { + // return (longName, longNameExpression?.GetLocation(), SuggestionType.Hidden); + // } } } return (null, null, SuggestionType.Hidden); diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index fce5a970..4f43a17f 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -1,6 +1,5 @@ using System.Diagnostics.Contracts; using DotNetCampus.Cli.Utils; -using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; From 30ce004ddd8cc2e7bba5d91321d437d59e3771e5 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 15:21:14 +0800 Subject: [PATCH 066/193] =?UTF-8?q?=E6=94=AF=E6=8C=81=20record=20command?= =?UTF-8?q?=20=E5=92=8C=E6=95=B0=E7=BB=84=20long=20option=20=E7=9A=84?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E6=B3=95=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 52 ++++++------ .../OptionLongNameMustBeKebabCaseAnalyzer.cs | 81 ++++++++++++------- ...nLongNameMustBeKebabCaseCodeFixProvider.cs | 9 +-- .../Properties/Localizations.Designer.cs | 4 +- .../Properties/Localizations.resx | 4 +- .../Properties/Localizations.zh-hans.resx | 4 +- 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index 743120fb..da95ec38 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -71,45 +71,47 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) foreach (var attributeSyntax in propertyNode.AttributeLists.SelectMany(x => x.Attributes)) { - string? attributeName = attributeSyntax.Name switch + var attributeName = attributeSyntax.Name switch { IdentifierNameSyntax identifierName => identifierName.ToString(), QualifiedNameSyntax qualifiedName => qualifiedName.ChildNodes().OfType().LastOrDefault()?.ToString(), _ => null, }; - if (attributeName != null) + if (attributeName == null) { - var attributeType = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - var isOptionAttributeType = optionTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); - var isRawArgumentsAttributeType = rawArgumentsTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); + continue; + } + + var attributeType = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; + var isOptionAttributeType = optionTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); + var isRawArgumentsAttributeType = rawArgumentsTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); + + // [Option], [Value] + if (isOptionAttributeType) + { + var isValidPropertyUsage = AnalyzeOptionPropertyType(context.SemanticModel, propertyNode); + var diagnostic = CreateDiagnosticForTypeSyntax( + isValidPropertyUsage + ? Diagnostics.DCL201_SupportedOptionPropertyType + : Diagnostics.DCL202_NotSupportedOptionPropertyType, + propertyNode); + context.ReportDiagnostic(diagnostic); + break; + } - // [Option], [Value] - if (isOptionAttributeType) + // [RawArguments] + if (isRawArgumentsAttributeType) + { + var isValidPropertyUsage = AnalyzeRawArgumentsPropertyType(context.SemanticModel, propertyNode); + if (!isValidPropertyUsage) { - var isValidPropertyUsage = AnalyzeOptionPropertyType(context.SemanticModel, propertyNode); var diagnostic = CreateDiagnosticForTypeSyntax( - isValidPropertyUsage - ? Diagnostics.DCL201_SupportedOptionPropertyType - : Diagnostics.DCL202_NotSupportedOptionPropertyType, + Diagnostics.DCL203_NotSupportedRawArgumentsPropertyType, propertyNode); context.ReportDiagnostic(diagnostic); break; } - - // [RawArguments] - if (isRawArgumentsAttributeType) - { - var isValidPropertyUsage = AnalyzeRawArgumentsPropertyType(context.SemanticModel, propertyNode); - if (!isValidPropertyUsage) - { - var diagnostic = CreateDiagnosticForTypeSyntax( - Diagnostics.DCL203_NotSupportedRawArgumentsPropertyType, - propertyNode); - context.ReportDiagnostic(diagnostic); - break; - } - } } } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs index 5d2d84ac..7bb4a40e 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs @@ -43,6 +43,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeRecord, SyntaxKind.RecordDeclaration); } /// @@ -55,6 +56,16 @@ private void AnalyzeClass(SyntaxNodeAnalysisContext context) AnalyzeAttribute(context, classNode.AttributeLists, true); } + /// + /// Find CommandAttribute from a property. + /// + /// + private void AnalyzeRecord(SyntaxNodeAnalysisContext context) + { + var classNode = (RecordDeclarationSyntax)context.Node; + AnalyzeAttribute(context, classNode.AttributeLists, true); + } + /// /// Find OptionAttribute from a property. /// @@ -77,18 +88,23 @@ private void AnalyzeAttribute(SyntaxNodeAnalysisContext context, SyntaxList - private (string? Name, Location? Location, SuggestionType SuggestionType) AnalyzeOptionAttributeArguments(AttributeSyntax attributeSyntax, - bool hasSeparator) + private IEnumerable<(string? Name, Location? Location, SuggestionType SuggestionType)> AnalyzeOptionAttributeArguments( + AttributeSyntax attributeSyntax, bool hasSeparator) { var argumentList = attributeSyntax.ChildNodes().OfType().FirstOrDefault(); - if (argumentList != null) + if (argumentList == null) + { + yield break; + } + + var attributeArguments = argumentList.ChildNodes().OfType().ToList(); + var optionNameArguments = attributeArguments.Where(x => x.NameEquals is null).ToList(); + var longNameExpressions = optionNameArguments.Count switch + { + 1 => optionNameArguments[0].DescendantNodes().OfType().ToList(), + 2 => optionNameArguments[1].DescendantNodes().OfType().ToList(), + _ => [], + }; + foreach (var longNameExpression in longNameExpressions) { - var attributeArguments = argumentList.ChildNodes().OfType().ToList(); - var longNameExpression = attributeArguments.FirstOrDefault()?.Expression as LiteralExpressionSyntax; var longName = longNameExpression?.Token.ValueText; - var caseSensitiveExpression = - attributeArguments.FirstOrDefault(x => x.NameEquals?.Name.ToString() == "CaseSensitive")?.Expression as LiteralExpressionSyntax; + var caseSensitiveExpression = attributeArguments + .FirstOrDefault(x => x.NameEquals?.Name.ToString() == "CaseSensitive")? + .Expression as LiteralExpressionSyntax; var caseSensitive = caseSensitiveExpression?.Token.ValueText.Equals("true", StringComparison.OrdinalIgnoreCase) is true; if (!caseSensitive && longName is not null) { // 严格检查。 - var kebabCase = MakeKebabCase(longName, true, true, hasSeparator); + var kebabCase = MakeKebabCase(longName, true, false, hasSeparator); var isKebabCase = string.Equals(kebabCase, longName, StringComparison.Ordinal); if (!isKebabCase) { - return (longName, longNameExpression?.GetLocation(), SuggestionType.Warning); + yield return (longName, longNameExpression?.GetLocation(), SuggestionType.Warning); } - // // 宽松检查。 - // var kebabCase2 = MakeKebabCase(longName, true, true, hasSeparator); - // var isKebabCase2 = string.Equals(kebabCase2, longName, StringComparison.Ordinal); - // if (!isKebabCase2) - // { - // return (longName, longNameExpression?.GetLocation(), SuggestionType.Hidden); - // } + // 宽松检查。 + var kebabCase2 = MakeKebabCase(longName, true, true, hasSeparator); + var isKebabCase2 = string.Equals(kebabCase2, longName, StringComparison.Ordinal); + if (!isKebabCase2) + { + yield return (longName, longNameExpression?.GetLocation(), SuggestionType.Info); + } } } - return (null, null, SuggestionType.Hidden); } private string MakeKebabCase(string oldName, bool isUpperSeparator, bool toLower, bool hasSeparator) @@ -151,7 +178,7 @@ private string MakeKebabCase(string oldName, bool isUpperSeparator, bool toLower private enum SuggestionType { - Hidden, + Info, Warning, } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs index b8e9a72a..9ad0f748 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs @@ -40,11 +40,10 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; - ExpressionSyntax? syntax = root.FindNode(diagnosticSpan) switch + var syntax = root.FindNode(diagnosticSpan) switch { - AttributeArgumentSyntax attributeArgumentSyntax => attributeArgumentSyntax.Expression, - ExpressionSyntax expressionSyntax => expressionSyntax, - _ => null, + LiteralExpressionSyntax expressionSyntax => expressionSyntax, + { } node => node.DescendantNodes().OfType().FirstOrDefault(), }; // 判断此 syntax 是属于 CommandAttribute 还是 OptionAttribute。 var attributeSyntax = syntax?.FirstAncestorOrSelf(); @@ -62,7 +61,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } } - private async Task MakeKebabCaseAsync(Document document, ExpressionSyntax expressionSyntax, + private async Task MakeKebabCaseAsync(Document document, LiteralExpressionSyntax expressionSyntax, bool hasSeparator, CancellationToken cancellationToken) { var expression = expressionSyntax.ToString(); diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs index b0bdf876..c252a546 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs @@ -69,7 +69,7 @@ public static string DCL101 { } /// - /// Looks up a localized string similar to The command-line option/command definition names should be kebab-case, even though you can use any kind of style in the command line environment.. + /// Looks up a localized string similar to The option/command name should be kebab-case nomenclature to disambiguate. /// public static string DCL101_Description { get { @@ -87,7 +87,7 @@ public static string DCL101_Fix1 { } /// - /// Looks up a localized string similar to The option/command definition long name '{0}' should be kebab-case, even though you can use any kind of style in the command line environment.. + /// Looks up a localized string similar to The option/command name should be kebab-case nomenclature to disambiguate. /// public static string DCL101_Message { get { diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx index c4794da6..4583fbf2 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx @@ -150,13 +150,13 @@ Not supported command-line property type - The command-line option/command definition names should be kebab-case, even though you can use any kind of style in the command line environment. + The option/command name should be kebab-case nomenclature to disambiguate Convert to kebab-case - The option/command definition long name '{0}' should be kebab-case, even though you can use any kind of style in the command line environment. + The option/command name should be kebab-case nomenclature to disambiguate Option/Command long name should be kebab-case diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx index e6d41df1..0d33603b 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx @@ -43,10 +43,10 @@ 不支持此类型的命令行属性 - 选项/命令名称 {0} 应该使用 kebab-case 命名法命名,即使真实使用命令行传参的时候你可以使用任意风格,但定义应该是 kebab-case 风格。 + 选项/命令名称 {0} 应使用 kebab-case 命名法以消除歧义 - 选项/命令名称的定义建议使用 kebab-case 命名法命名,即使真实使用命令行传参的时候你可以使用任意风格,但定义也应该是 kebab-case 风格。 + 选项/命令名称应使用 kebab-case 命名法以消除歧义 改成 kebab-case 命名法 From 88cdfb82e62ba76123353700bf2c8b2a5fedb586 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 16:48:25 +0800 Subject: [PATCH 067/193] =?UTF-8?q?=E5=8A=A0=E5=85=A5=204.0=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E7=9A=84=E6=B5=8B=E8=AF=95=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 1 + Nuget.config | 6 +++ ...CommandLine.Temp40.4.0.1-benchmark.1.nupkg | Bin 0 -> 307669 bytes ...ommandLine.Temp40.4.0.1-benchmark.1.snupkg | Bin 0 -> 375227 bytes ...otNetCampus.CommandLine.Performance.csproj | 1 + .../Fakes/BenchmarkOptions40.cs | 46 ++++++++++++++++++ ...hmarkOptions4.cs => BenchmarkOptions41.cs} | 6 +-- .../ParseArgs/ParseCmdArgs.cs | 27 +++++++--- .../ParseArgs/ParseDotNetArgs.cs | 28 ++++++----- .../ParseArgs/ParseGnuArgs.cs | 27 +++++++--- .../ParseArgs/ParseMixArgs.cs | 16 ++++-- .../ParseArgs/ParseNoArgs.cs | 27 +++++++--- .../ParseArgs/ParsePowerShellArgs.cs | 27 +++++++--- .../Properties/GlobalUsings.cs | 5 ++ 14 files changed, 170 insertions(+), 47 deletions(-) create mode 100644 Nuget.config create mode 100644 tests/DotNetCampus.CommandLine.Performance/CompareTo/DotNetCampus.CommandLine.Temp40.4.0.1-benchmark.1.nupkg create mode 100644 tests/DotNetCampus.CommandLine.Performance/CompareTo/DotNetCampus.CommandLine.Temp40.4.0.1-benchmark.1.snupkg create mode 100644 tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions40.cs rename tests/DotNetCampus.CommandLine.Performance/Fakes/{BenchmarkOptions4.cs => BenchmarkOptions41.cs} (92%) create mode 100644 tests/DotNetCampus.CommandLine.Performance/Properties/GlobalUsings.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d7e50ac..9b6438e6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ + diff --git a/Nuget.config b/Nuget.config new file mode 100644 index 00000000..d7068e80 --- /dev/null +++ b/Nuget.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/DotNetCampus.CommandLine.Performance/CompareTo/DotNetCampus.CommandLine.Temp40.4.0.1-benchmark.1.nupkg b/tests/DotNetCampus.CommandLine.Performance/CompareTo/DotNetCampus.CommandLine.Temp40.4.0.1-benchmark.1.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..4d8c2cdc5e9119c4289f4e796ecb54d1fe8551ca GIT binary patch literal 307669 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 z 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

`9(ksOJ9qhVX?uA4l{aKIr5~nCLevMm)~Q> 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 XXec8o!X?(PnQJ2S)b@6&GW!@hJ^ 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�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

`) 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

-&;MMFBqu*L)&HJ`a7qDUnA}#6pS23@R&j9 z`oRNt6i^}=WJkKMt7KqNkJdE<5XiJ`)1Ywq^SpG^{|2of0}X=-002OMU%_lzjM<)X z3-6_=cioAP?`JI$TN_6c8%I55H;9RY4wI|3Rpyk?*FF|x*vS`3hY(t=+2R^OP(v0h z1eyt5sVPd*iosc8^Y*)`I@xE^+x0h%$Ctc4<7fp+4Ak4k(;q-&b}?yMci$G1z%nAn~4w(51QIjyxL1tOM4W%Haguj9>t zdrz7Q_loE@V3}k4(o1CScTncpN^sl6g7U zmJ0R;;f9OfC-unT_F#qk`|El+yFabCosT0$<-GUu?F}06PafXcp?Z1eU}???_KxQ77udd-el}t?GGR0R zY{JgL_SwMH*w~1jn~UYMp|O!My9pD-!P?jeVihkJgCT?-d`|fsGMt&`A+Aoz^_x2X za)XI14!3$t7D!awO0&j~v^rn?{kbnN0KXuW1GwEE5oRFf6X|){$s(&yIdD@nbNck#i%X$F6`8*GdtG5N;O)0nYCS@rAvT2 ztW?FmNo-XR55gKn3`2K~ft4e}o1%9Hg)NXVE^=Y9K+AdNBm8kpMoE~L<;-G59=wyz zhRF|dY^9te4fqIu&VNQql1O7-TDEcKd27j$`Kcse?xOeVQvcG_{&M&4m1A26SgY-+ z<@Et_xpVFLMYG+jCKXk!ci7^}@w3-2;vB|xH0&7hip%SVyX)PiRva_tV>??HOv=L*0)ph%LhExOB=NB0rpwD5934e)_HTv!9rl*UIdC zFD~fB3c7j0RMa{4bR!GXf#|>`=qBlSV+S7zsQ9RUPayMVv0f?8DOjNSJeSzSI&gYw zuOmLcXB;Vwc_1mACQ?X+z$(sY0W;f*@X~SsVBS9wXH(;&MF*=5iJWa9mg`m%h#7O= z=V%`$evCOkH*E3wzqSU1!UX)!to!3Szr^rvZ7A>7hV*{VKQ}#l6DtR1rhgq3Wd2!^ z{de51)O#fw7yy9!AGMU<;Y4j6znVCT7+6D`9GFCGt*s4gjAhMjOqi5StRd_yOg2sq z5EG++V2ZP8O%&tFBU1nYFxEf-(?2jbp#cCZb3{4Aj4|x&AwB*w7K&fOk&5zjMOz4^F5c z(ErB%mnSfQK-ic*|2Nd+e?SRtAqE1}Er5V8|7hYV0p{OLI5@r!p@F?I>wnRW|HZri xPcsO0u>YH{|Ht^f1N^63{2zc1^|1f{ouq;cEZo1EKzQG2-;K|!5%#_Ge*l^R$;$u$ literal 0 HcmV?d00001 diff --git a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj index 98a4e8ff..e905b5f3 100644 --- a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj +++ b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj @@ -20,6 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions40.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions40.cs new file mode 100644 index 00000000..2b9f1466 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions40.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using DotNetCampus.Cli.Temp40.Compiler; + +namespace DotNetCampus.Cli.Performance.Fakes; + +public class BenchmarkOptions40 +{ + [Option("debug")] + public required bool IsDebugMode { get; init; } + + [Option('c', "count")] + public required int TestCount { get; init; } + + [Option('n', "test-name")] + public string? TestName { get; set; } + + [Option("test-category")] + public string? TestCategory { get; set; } + + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; + + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} + +public class NullableBenchmarkOptions40 +{ + [Option("debug")] + public bool IsDebugMode { get; set; } + + [Option('c', "count")] + public int TestCount { get; set; } + + [Option('n', "test-name")] + public string? TestName { get; set; } + + [Option("test-category")] + public string? TestCategory { get; set; } + + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; + + [Value(0, int.MaxValue)] + public IList TestItems { get; set; } = null!; +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions41.cs similarity index 92% rename from tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs rename to tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions41.cs index 330fb973..316a9fab 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions4.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/BenchmarkOptions41.cs @@ -4,7 +4,7 @@ namespace DotNetCampus.Cli.Performance.Fakes; [Command("", ExperimentalUseFullStackParser = true)] -public readonly record struct FullStackBenchmarkOptions4() +public readonly record struct FullStackBenchmarkOptions41() { [Option("debug")] public required bool IsDebugMode { get; init; } @@ -25,7 +25,7 @@ public readonly record struct FullStackBenchmarkOptions4() public IReadOnlyList TestItems { get; init; } = null!; } -public class BenchmarkOptions4 +public class BenchmarkOptions41 { [Option("debug")] public required bool IsDebugMode { get; init; } @@ -46,7 +46,7 @@ public class BenchmarkOptions4 public IReadOnlyList TestItems { get; init; } = null!; } -public class NullableBenchmarkOptions4 +public class NullableBenchmarkOptions41 { [Option("debug")] public bool IsDebugMode { get; set; } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs index 10f72a28..5bbf9ce3 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; // ReSharper disable ReturnValueOfPureMethodIsNotUsed @@ -14,28 +13,42 @@ public class ParseCmdArgs [Benchmark(Description = "parse [CMD] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(CmdArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(CmdArgs, Options41.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [CMD] -v=4.1 -p=powershell")] public void Parse41_PowerShell() { - var commandLine = CommandLine.Parse(CmdArgs, PowerShell); - commandLine.As(); + var commandLine = CommandLine41.Parse(CmdArgs, Options41.PowerShell); + commandLine.As(); + } + + [Benchmark(Description = "parse [CMD] -v=4.0 -p=flexible")] + public void Parse40_Flexible() + { + var commandLine = CommandLine40.Parse(CmdArgs, Options40.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [CMD] -v=4.0 -p=powershell")] + public void Parse40_PowerShell() + { + var commandLine = CommandLine40.Parse(CmdArgs, Options40.PowerShell); + commandLine.As(); } [Benchmark(Description = "parse [CMD] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdArgs); + var commandLine = CommandLine3.Parse(CmdArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [CMD] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdArgs); + var commandLine = CommandLine3.Parse(CmdArgs); commandLine.As(); } } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs index 4e86b4a8..8ed2eaf1 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs @@ -2,7 +2,6 @@ using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; // ReSharper disable ReturnValueOfPureMethodIsNotUsed @@ -15,35 +14,42 @@ public class ParseDotNetArgs [Benchmark(Description = "parse [NET] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(DotNetArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(DotNetArgs, Options41.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [NET] -v=4.1 -p=dotnet")] public void Parse41_Dotnet() { - var commandLine = CommandLine.Parse(DotNetArgs, DotNet); - commandLine.As(); + var commandLine = CommandLine41.Parse(DotNetArgs, Options41.DotNet); + commandLine.As(); } - [Benchmark(Description = "parse [NET] -v=4.1 -p=dotnet (struct)")] - public void Parse41_Dotnet_Struct() + [Benchmark(Description = "parse [NET] -v=4.0 -p=flexible")] + public void Parse40_Flexible() { - var commandLine = CommandLine.Parse(DotNetArgs, DotNet); - var o = commandLine.As(); + var commandLine = CommandLine40.Parse(DotNetArgs, Options40.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [NET] -v=4.0 -p=dotnet")] + public void Parse40_Dotnet() + { + var commandLine = CommandLine40.Parse(DotNetArgs, Options40.DotNet); + commandLine.As(); } [Benchmark(Description = "parse [NET] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetArgs); + var commandLine = CommandLine3.Parse(DotNetArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [NET] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(DotNetArgs); + var commandLine = CommandLine3.Parse(DotNetArgs); commandLine.As(); } } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index 9a732385..f1482607 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -3,7 +3,6 @@ using ConsoleAppFramework; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; #if IS_NOT_USING_AOT using System.Collections.Generic; @@ -24,28 +23,42 @@ public class ParseGnuArgs [Benchmark(Description = "parse [GNU] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(GnuArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(GnuArgs, Options41.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [GNU] -v=4.1 -p=gnu")] public void Parse41_PowerShell() { - var commandLine = CommandLine.Parse(GnuArgs, Gnu); - commandLine.As(); + var commandLine = CommandLine41.Parse(GnuArgs, Options41.Gnu); + commandLine.As(); + } + + [Benchmark(Description = "parse [GNU] -v=4.0 -p=flexible")] + public void Parse40_Flexible() + { + var commandLine = CommandLine40.Parse(GnuArgs, Options40.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [GNU] -v=4.0 -p=gnu")] + public void Parse40_PowerShell() + { + var commandLine = CommandLine40.Parse(GnuArgs, Options40.Gnu); + commandLine.As(); } [Benchmark(Description = "parse [GNU] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuArgs); + var commandLine = CommandLine3.Parse(GnuArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [GNU] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(GnuArgs); + var commandLine = CommandLine3.Parse(GnuArgs); commandLine.As(); } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs index 17e0eb7e..27ce8eee 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; // ReSharper disable ReturnValueOfPureMethodIsNotUsed @@ -14,21 +13,28 @@ public class ParseMixArgs [Benchmark(Description = "parse [MIX] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(MixArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(MixArgs, Options41.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [MIX] -v=4.0 -p=flexible")] + public void Parse40_Flexible() + { + var commandLine = CommandLine40.Parse(MixArgs, Options40.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [MIX] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(MixArgs); + var commandLine = CommandLine3.Parse(MixArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [MIX] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(MixArgs); + var commandLine = CommandLine3.Parse(MixArgs); commandLine.As(); } } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs index 0fa4009d..2e59fcf1 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseNoArgs.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; // ReSharper disable ReturnValueOfPureMethodIsNotUsed @@ -14,28 +13,42 @@ public class ParseNoArgs [Benchmark(Description = "parse [] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(NoArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(NoArgs, Options41.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [] -v=4.1 -p=dotnet")] public void Parse41_PowerShell() { - var commandLine = CommandLine.Parse(NoArgs, DotNet); - commandLine.As(); + var commandLine = CommandLine41.Parse(NoArgs, Options41.DotNet); + commandLine.As(); + } + + [Benchmark(Description = "parse [] -v=4.0 -p=flexible")] + public void Parse40_Flexible() + { + var commandLine = CommandLine40.Parse(NoArgs, Options40.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [] -v=4.0 -p=dotnet")] + public void Parse40_PowerShell() + { + var commandLine = CommandLine40.Parse(NoArgs, Options40.DotNet); + commandLine.As(); } [Benchmark(Description = "parse [] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); + var commandLine = CommandLine3.Parse(NoArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); + var commandLine = CommandLine3.Parse(NoArgs); commandLine.As(); } } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs index 8f451556..fe882660 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using DotNetCampus.Cli.Performance.Fakes; using static DotNetCampus.Cli.Performance.Fakes.CommandLineArguments; -using static DotNetCampus.Cli.CommandLineParsingOptions; // ReSharper disable ReturnValueOfPureMethodIsNotUsed @@ -14,28 +13,42 @@ public class ParsePowerShellArgs [Benchmark(Description = "parse [PS1] -v=4.1 -p=flexible")] public void Parse41_Flexible() { - var commandLine = CommandLine.Parse(PowerShellArgs, Flexible); - commandLine.As(); + var commandLine = CommandLine41.Parse(PowerShellArgs, Options41.Flexible); + commandLine.As(); } [Benchmark(Description = "parse [PS1] -v=4.1 -p=powershell")] public void Parse41_PowerShell() { - var commandLine = CommandLine.Parse(PowerShellArgs, PowerShell); - commandLine.As(); + var commandLine = CommandLine41.Parse(PowerShellArgs, Options41.PowerShell); + commandLine.As(); + } + + [Benchmark(Description = "parse [PS1] -v=4.0 -p=flexible")] + public void Parse40_Flexible() + { + var commandLine = CommandLine40.Parse(PowerShellArgs, Options40.Flexible); + commandLine.As(); + } + + [Benchmark(Description = "parse [PS1] -v=4.0 -p=powershell")] + public void Parse40_PowerShell() + { + var commandLine = CommandLine40.Parse(PowerShellArgs, Options40.PowerShell); + commandLine.As(); } [Benchmark(Description = "parse [PS1] -v=3.x -p=parser")] public void Parse3x_Parser() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellArgs); + var commandLine = CommandLine3.Parse(PowerShellArgs); commandLine.As(new BenchmarkOption3Parser()); } [Benchmark(Description = "parse [PS1] -v=3.x -p=runtime")] public void Parse3x_Runtime() { - var commandLine = dotnetCampus.Cli.CommandLine.Parse(PowerShellArgs); + var commandLine = CommandLine3.Parse(PowerShellArgs); commandLine.As(); } } diff --git a/tests/DotNetCampus.CommandLine.Performance/Properties/GlobalUsings.cs b/tests/DotNetCampus.CommandLine.Performance/Properties/GlobalUsings.cs new file mode 100644 index 00000000..830e6018 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/Properties/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using CommandLine3 = dotnetCampus.Cli.CommandLine; +global using CommandLine41 = DotNetCampus.Cli.CommandLine; +global using Options41 = DotNetCampus.Cli.CommandLineParsingOptions; +global using CommandLine40 = DotNetCampus.Cli.Temp40.CommandLine; +global using Options40 = DotNetCampus.Cli.Temp40.CommandLineParsingOptions; From b7011a5dd7311a616d0c8c42e9e85b62f9d0e4f8 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 17:19:49 +0800 Subject: [PATCH 068/193] =?UTF-8?q?=E9=80=82=E9=85=8D=E8=AE=A9=20Temp40=20?= =?UTF-8?q?=E8=83=BD=E8=B7=91=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fakes/CommandLineArguments.cs | 33 +++++++++++++++++++ .../ParseArgs/ParseCmdArgs.cs | 4 +-- .../ParseArgs/ParseDotNetArgs.cs | 4 +-- .../ParseArgs/ParseMixArgs.cs | 2 +- .../ParseArgs/ParsePowerShellArgs.cs | 4 +-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs index e1f5562e..e74222c6 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs @@ -15,6 +15,17 @@ internal static class CommandLineArguments "--debug", ]; + public static readonly string[] DotNetArgsFor40 = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "--count:20", + "--test-name:BenchmarkTest", + "--detail-level=High", + "--debug", + ]; + public static readonly string[] PowerShellArgs = [ "DotNetCampus.CommandLine.Performance.dll", @@ -26,6 +37,17 @@ internal static class CommandLineArguments "-Debug", ]; + public static readonly string[] PowerShellArgsFor40 = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "-Count", "20", + "-TestName", "BenchmarkTest", + "-DetailLevel", "High", + "-Debug", + ]; + public static readonly string[] CmdArgs = [ "DotNetCampus.CommandLine.Performance.dll", @@ -67,4 +89,15 @@ internal static class CommandLineArguments "--detail-level=High", "-Debug", ]; + + public static readonly string[] MixArgsFor40 = + [ + "DotNetCampus.CommandLine.Performance.dll", + "DotNetCampus.CommandLine.Sample.dll", + "DotNetCampus.CommandLine.Test.dll", + "--count:20", + "/TestName", "BenchmarkTest", + "--detail-level=High", + "-Debug", + ]; } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs index 5bbf9ce3..5bcc01da 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseCmdArgs.cs @@ -31,10 +31,10 @@ public void Parse40_Flexible() commandLine.As(); } - [Benchmark(Description = "parse [CMD] -v=4.0 -p=powershell")] + [Benchmark(Description = "parse [CMD] -v=4.0 -p=dotnet")] public void Parse40_PowerShell() { - var commandLine = CommandLine40.Parse(CmdArgs, Options40.PowerShell); + var commandLine = CommandLine40.Parse(CmdArgs, Options40.DotNet); commandLine.As(); } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs index 8ed2eaf1..f4fa5a26 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseDotNetArgs.cs @@ -28,14 +28,14 @@ public void Parse41_Dotnet() [Benchmark(Description = "parse [NET] -v=4.0 -p=flexible")] public void Parse40_Flexible() { - var commandLine = CommandLine40.Parse(DotNetArgs, Options40.Flexible); + var commandLine = CommandLine40.Parse(DotNetArgsFor40, Options40.Flexible); commandLine.As(); } [Benchmark(Description = "parse [NET] -v=4.0 -p=dotnet")] public void Parse40_Dotnet() { - var commandLine = CommandLine40.Parse(DotNetArgs, Options40.DotNet); + var commandLine = CommandLine40.Parse(DotNetArgsFor40, Options40.DotNet); commandLine.As(); } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs index 27ce8eee..ebe04d80 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseMixArgs.cs @@ -20,7 +20,7 @@ public void Parse41_Flexible() [Benchmark(Description = "parse [MIX] -v=4.0 -p=flexible")] public void Parse40_Flexible() { - var commandLine = CommandLine40.Parse(MixArgs, Options40.Flexible); + var commandLine = CommandLine40.Parse(MixArgsFor40, Options40.Flexible); commandLine.As(); } diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs index fe882660..f54a1502 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParsePowerShellArgs.cs @@ -27,14 +27,14 @@ public void Parse41_PowerShell() [Benchmark(Description = "parse [PS1] -v=4.0 -p=flexible")] public void Parse40_Flexible() { - var commandLine = CommandLine40.Parse(PowerShellArgs, Options40.Flexible); + var commandLine = CommandLine40.Parse(PowerShellArgsFor40, Options40.Flexible); commandLine.As(); } [Benchmark(Description = "parse [PS1] -v=4.0 -p=powershell")] public void Parse40_PowerShell() { - var commandLine = CommandLine40.Parse(PowerShellArgs, Options40.PowerShell); + var commandLine = CommandLine40.Parse(PowerShellArgsFor40, Options40.PowerShell); commandLine.As(); } From 41a092dfa8eef44e5f74392f602572d54599ed90 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 17:52:56 +0800 Subject: [PATCH 069/193] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=BA=BF=E4=B8=8A?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b6438e6..6bb5efce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + @@ -17,4 +17,4 @@ - \ No newline at end of file + From 0449de31ff1e9cd445ad1e5e412f967c23fd0e78 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 18:36:37 +0800 Subject: [PATCH 070/193] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=20AI=20=E7=9A=84?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E6=84=8F=E8=A7=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs | 6 +----- .../Utils/Parsers/CommandLineParser.cs | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 81be177b..6c529888 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -141,11 +141,7 @@ private static IReadOnlyList ParseCommandAndPositionalArguments(ReadOnly } var parts = argument.ToString().Split(['/'], StringSplitOptions.RemoveEmptyEntries); - var result = new List(parts.Length); - foreach (var part in parts) - { - result.Add(Uri.UnescapeDataString(part)); - } + var result = parts.Select(Uri.UnescapeDataString).ToList(); return result; } diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 00152121..7f58859e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -24,10 +24,10 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int _commandLine = commandLine; _commandObjectName = commandObjectName; _commandCount = commandCount; - var isUrl = commandLine.MatchedUrlScheme is null; + var isUrl = commandLine.MatchedUrlScheme is not null; Style = isUrl - ? commandLine.ParsingOptions.Style - : CommandLineParsingOptions.UrlStyle; + ? CommandLineParsingOptions.UrlStyle + : commandLine.ParsingOptions.Style; _namingPolicy = Style.NamingPolicy; OptionPrefix = Style.OptionPrefix; _caseSensitive = Style.CaseSensitive; From bff2d0b98f3edb89815db2b8a2a63615f6e14496 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 18:37:29 +0800 Subject: [PATCH 071/193] =?UTF-8?q?=E4=BF=AE=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dotnet-build.yml | 4 +--- .github/workflows/nuget-tag-publish.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 69b84ffe..d6f41990 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -13,9 +13,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x - 6.0.x - 8.0.x + 9.0.x - name: Build run: dotnet build --configuration Release diff --git a/.github/workflows/nuget-tag-publish.yml b/.github/workflows/nuget-tag-publish.yml index 40efc1a7..88359c5e 100644 --- a/.github/workflows/nuget-tag-publish.yml +++ b/.github/workflows/nuget-tag-publish.yml @@ -17,9 +17,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x - 6.0.x - 8.0.x + 9.0.x - name: Install dotnet tool run: dotnet tool install -g dotnetCampus.TagToVersion From e0e224c357b9dfd5a7c68919945f81044037a85f Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 20:19:59 +0800 Subject: [PATCH 072/193] =?UTF-8?q?=E9=87=8D=E6=96=B0=E7=BC=96=E5=86=99?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BB=A5=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=9C=80=E6=96=B0=E7=9A=84=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-hans/README.md | 821 +++++++++++++++++++++-------------------- 1 file changed, 418 insertions(+), 403 deletions(-) diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index a6994937..4b69a5a1 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -31,46 +31,44 @@ class Program 你需要定义一个包含命令行参数映射的类型: ```csharp -class Options +public class Options { - [Value(0)] - public required string FilePath { get; init; } + [Option("debug")] + public required bool IsDebugMode { get; init; } - [Option('s', "silence")] - public bool IsSilence { get; init; } + [Option('c', "count")] + public required int TestCount { get; init; } - [Option('m', "mode")] - public string? StartMode { get; init; } + [Option('n', "test-name")] + public string? TestName { get; set; } - [Option("startup-sessions")] - public IReadOnlyList StartupSessions { get; init; } = []; -} -``` + [Option("test-category")] + public string? TestCategory { get; set; } -然后在命令行中使用不同风格的命令填充这个类型的实例。库支持多种命令行风格: - -### Windows PowerShell 风格 - -```powershell -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s -Mode Edit -StartupSessions A B C -``` + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; -### Windows CMD 风格 + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} -```cmd -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" /s /Mode Edit /StartupSessions A B C +public enum DetailLevel +{ + Low, + Medium, + High, +} ``` -### Linux/GNU 风格 - -```bash -$ demo.exe "C:/Users/lvyi/Desktop/demo.txt" -s --mode Edit --startup-sessions A --startup-sessions B --startup-sessions C -``` +然后在命令行中使用不同风格的命令填充这个类型的实例。库支持多种命令行风格: -### .NET CLI 风格 -``` -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s:true --mode:Edit --startup-sessions:A;B;C -``` +| 风格 | 示例 | +| -------------- | ------------------------------------------------------------------------------------------ | +| DotNet | `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` | +| PowerShell | `demo.exe 1.txt 2.txt 3.txt -c 20 -TestName BenchmarkTest -DetailLevel High -Debug` | +| CMD | `demo.exe 1.txt 2.txt 3.txt /c 20 /TestName BenchmarkTest /DetailLevel High /Debug` | +| Gnu | `demo.exe 1.txt 2.txt 3.txt -c 20 --test-name BenchmarkTest --detail-level High --debug` | +| 灵活(Flexible) | `demo.exe 1.txt 2.txt 3.txt --count:20 /TestName BenchmarkTest --detail-level=High -Debug` | ## 命令行风格 @@ -83,178 +81,135 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); 支持的风格包括: -- `CommandLineStyle.Flexible`(默认):智能识别多种风格,默认大小写不敏感,是 DotNet/GNU/PowerShell 风格的有效组合 - - 支持前面示例中所有风格的命令行参数,可正确解析 - - 完整支持 DotNet 风格的所有命令行功能(包括列表和字典) - - 支持 GNU 风格中除短名称接参数(如 `-o1.txt`)和短名称缩写(如 `-abc` 表示 `-a -b -c`)外的所有功能 - - 由于 Posix 规则限制严格,Flexible 风格自然兼容 Posix 风格 - - DotNet 风格本身兼容 PowerShell 命令行风格,因此 Flexible 风格也支持 PowerShell 风格 +- `CommandLineStyle.Flexible`(默认):灵活风格,在各种风格间提供最大的兼容性,默认大小写不敏感 +- `CommandLineStyle.DotNet`:.NET CLI 风格,默认大小写敏感 - `CommandLineStyle.Gnu`:符合 GNU 规范的风格,默认大小写敏感 - `CommandLineStyle.Posix`:符合 POSIX 规范的风格,默认大小写敏感 -- `CommandLineStyle.DotNet`:.NET CLI 风格,默认大小写不敏感 - `CommandLineStyle.PowerShell`:PowerShell 风格,默认大小写不敏感 -## 数据类型支持 - -库支持多种数据类型的解析: - -1. **基本类型**: 字符串、整数、布尔值、枚举等 -2. **集合类型**: 数组、列表、只读集合、不可变集合 -3. **字典类型**: IDictionary、IReadOnlyDictionary、ImmutableDictionary等 - -### 布尔类型选项 - -对于布尔类型的选项,在命令行中有多种指定方式: - -- 仅指定选项名称,表示 `true`:`-s` 或 `--silence` -- 显式指定值:`-s:true`、`-s=false`、`--silence:on`、`--silence=off` - -### 集合类型选项 - -对于集合类型的选项,可以通过多次指定同一选项,或使用分号分隔多个值: - -``` -demo.exe --files file1.txt --files file2.txt -demo.exe --files:file1.txt;file2.txt;file3.txt -``` - -### 字典类型选项 - -对于字典类型的选项,支持多种传入方式: - -``` -demo.exe --properties key1=value1 --properties key2=value2 -demo.exe --properties:key1=value1;key2=value2 -``` - -## 位置参数 - -除了命名选项外,你还可以使用位置参数,通过 `ValueAttribute` 指定参数的位置: +默认情况下,这些风格的详细区别如下: + +| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ---------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | +| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | +| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | +| 选项值 `=` | -o=value | -o=value | -o=value | | | option=value | +| 选项值 `:` | -o:value | -o:value | | | | | +| 选项值 ` ` | -o value | -o value | -o value | -o value | -o value | | +| 布尔选项 | -o | -o | -o | -o | -o | option | +| 布尔选项 | -o=true | -o=true | | | -o:true | option=true | +| 布尔值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布尔值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布尔值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布尔值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | +| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | +| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | +| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 多短布尔选项合并 | 不支持 | 不支持 | -abc | -abc | 不支持 | 不支持 | +| 单短选项多字符 | -ab | -ab | 不支持 | 不支持 | -ab | 不支持 | +| 短选项直接带值 | 不支持 | 不支持 | -o1.txt | 不支持 | 不支持 | 不支持 | +| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | +| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | + +## 命名法 + +1. 在代码中定义一个选项时,你应该使用 kebab-case 命名法 + - [为什么要这么做?](https://github.com/dotnet-campus/DotNetCampus.CommandLine/blob/main/docs/analyzers/DCL101.md) + - 如果我们猜测你写的不是 kebab-case 命名法,会提供一个警告 DCL101 + - 但你可以忽略这个警告,无论你最终写了什么字符串,我们都视你写的是 kebab-case 命名法(这可以提供无歧义的命名信息,见下例) +2. 当你在代码中定义了被视为 kebab-case 命名法的字符串后 + - 根据你设置的不同命令行解析风格,你可以使用 kebab-case PascalCase 和 camelCase 三种风格的命名法 + +例如你定义了如下命令行对象: ```csharp -class FileOptions +[Command("open command-line")] +public class Options { - [Value(0)] - public string InputFile { get; init; } - - [Value(1)] - public string OutputFile { get; init; } - - [Option('v', "verbose")] - public bool Verbose { get; init; } + [Option('o', "option-name")] + public required string OptionName { get; init; } } ``` -使用方式: +这里存在两个使用了 kebab-case 命名法的地方,一个是 `Command` 特性,另一个是 `Option` 特性。你可以接受以下这些命令行传入: -``` -demo.exe input.txt output.txt --verbose -``` +- DotNet/Gnu 风格: `demo.exe open command-line --option-name value` +- PowerShell 风格: `demo.exe Open CommandLine -OptionName value` +- CMD 风格: `demo.exe Open CommandLine /optionName value` -你也可以捕获多个位置参数到一个数组或集合中: +但加入你把这两处的名字都写成其他风格,你可能会获得不太符合预期的结果(当然,也可能你故意如此): ```csharp -class MultiFileOptions +#pragma warning disable DCL101 +[Command("Open CommandLine")] +public class Options { - [Value(0, Length = int.MaxValue)] - public string[] Files { get; init; } = []; + // 此时会有分析器警告,OptionName 不是 kebab-case 风格。如果需要,你可以抑制 DCL101。 + [Option('o', "OptionName")] + public required string OptionName { get; init; } } +#pragma warning restore DCL101 ``` -## 组合使用选项和位置参数 +由于我们视这些都是 kebab-case 风格,所以你将接受以下这些命令行传入(注意 DotNet/Gnu 风格已经发生了变化): -`ValueAttribute` 和 `OptionAttribute` 可以同时应用于同一个属性: +- DotNet/Gnu 风格: `demo.exe Open CommandLine --OptionName value` +- PowerShell 风格: `demo.exe Open CommandLine -OptionName value` +- CMD 风格: `demo.exe Open CommandLine /optionName value` -```csharp -class Options -{ - [Value(0), Option('f', "file")] - public string FilePath { get; init; } -} -``` +## 数据类型 -这样,以下命令行都会将文件路径赋值给 `FilePath` 属性: - -``` -demo.exe file.txt -demo.exe -f file.txt -demo.exe --file file.txt -``` - -## 必需选项与可选选项 - -在C# 11及以上版本中,可以使用`required`修饰符标记必需的选项: - -```csharp -class Options -{ - [Option('i', "input")] - public required string InputFile { get; init; } // 必需选项 - - [Option('o', "output")] - public string? OutputFile { get; init; } // 可选选项 -} -``` +库支持多种数据类型的解析: -如果未提供必需选项,解析时会抛出`RequiredPropertyNotAssignedException`异常。 +1. **基本类型**: 字符串、整数、布尔值、枚举等 +2. **集合类型**: 数组、列表、只读集合、不可变集合 +3. **字典类型**: `IDictionary`、`IReadOnlyDictionary`、`ImmutableDictionary` 等 -## 属性初始值与访问器修饰符 +关于这些类型如何通过命令行传入,请见上表(最详细的那个)。 -在定义选项类型时,需要注意属性初始值与访问器修饰符(`init`、`required`)之间的关系: +## 必需选项与默认值 -```csharp -class Options -{ - // 错误示例:当使用 init 或 required 时,默认值将被忽略 - [Option('f', "format")] - public string Format { get; init; } = "json"; // 默认值不会生效! - - // 正确示例:使用 set 以保留默认值 - [Option('f', "format")] - public string Format { get; set; } = "json"; // 默认值会正确保留 -} -``` +当你定义一个属性的时候,有这些标记可用: -### 关于属性初始值的重要说明 +1. 使用 `required` 标记一个选项是必须的 +1. 使用 `init` 标记一个选项是不可变的 +1. 使用 `?` 标记一个选项是可空的 -1. **使用 `init` 或 `required` 时的行为**: - - 当属性包含 `required` 或 `init` 修饰符时,属性的初始值会被忽略 - - 如果命令行参数中未提供该选项的值,属性将被设置为 `default(T)`(对于引用类型为 `null`) - - 这是由 C# 语言特性决定的,命令行库如果希望突破此限制需要针对所有属性排列组合进行处理,显然是非常浪费的 +而具体会被赋成什么值取决于以下这些因素: -2. **保留默认值的方式**: - - 如果需要为属性提供默认值,应使用 `{ get; set; }` 而非 `{ get; init; }` +| required | init | 集合属性 | nullable | 行为 | 解释 | +| -------- | ---- | -------- | -------- | -------- | ----------------------------------- | +| 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | +| 0 | 1 | 1 | _ | 空集合 | 集合永不为 `null`,没传就赋值空集合 | +| 0 | 1 | 0 | 1 | `null` | 可空,没有传就赋值 `null` | +| 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | +| 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | -3. **可空类型与警告处理**: - - 对于非必需的引用类型属性,应将其标记为可空(如 `string?`)以避免可空警告 - - 对于值类型(如 `int`、`bool`),如果想保留默认值而非 `null`,不应将其标记为可空 +- 1 = 标记了 +- 0 = 没标记 +- _ = 无论有没有标记 -示例: +1. 可空,无论是引用类型还是值类型,其行为完全一致。要硬说不同,就是那个「默认值」会导致引用类型得到 `null`。 +2. 如果未提供必需选项,解析时会抛出`RequiredPropertyNotAssignedException`异常。 +3. 上述行为的「保留初值」的意思是,你可以在定义这个属性的时候写一个初值,就像下面这样: ```csharp -class OptionsBestPractice -{ - // 必需选项:使用 required,无需担心默认值 - [Option("input")] - public required string InputFile { get; init; } - - // 可选选项:标记为可空类型以避免警告 - [Option("output")] - public string? OutputFile { get; init; } - - // 需要默认值的选项:使用 set 而非 init - [Option("format")] - public string Format { get; set; } = "json"; - - // 值类型选项:不需要标记为可空 - [Option("count")] - public int Count { get; set; } = 1; -} +// 请注意,这里的初值仅在没有 required 也没有 init 时才生效。 +[Option('o', "option-name")] +public string OptionName { get; set; } = "Default Value" ``` -## 命令处理与命令 +## 命令与子命令 -你可以使用命令处理器模式处理不同的命令,类似于`git commit`、`git push`等。DotNetCampus.CommandLine 提供了多种添加命令处理器的方式: +你可以使用命令处理器模式处理不同的命令,类似于`git commit`、`git remote add`等。DotNetCampus.CommandLine 提供了多种添加命令处理器的方式: ### 1. 使用委托处理命令 @@ -295,13 +250,13 @@ internal class ConvertCommandHandler : ICommandHandler { [Option('i', "input")] public required string InputFile { get; init; } - + [Option('o', "output")] public string? OutputFile { get; init; } - + [Option('f', "format")] public string Format { get; set; } = "json"; - + public Task RunAsync() { // 实现命令处理逻辑 @@ -316,262 +271,346 @@ internal class ConvertCommandHandler : ICommandHandler ```csharp var commandLine = CommandLine.Parse(args); -commandLine.AddHandler() +commandLine + .AddHandler() + .AddHandler() + .AddHandler(options => { /* 处理remove命令 */ }) .Run(); ``` -### 3. 使用程序集自动发现命令处理器 +### 一些说明 -为了更方便地管理大量命令且无需手动逐个添加,可以使用程序集自动发现功能,自动添加程序集中所有实现了 `ICommandHandler` 接口的类: - -```csharp -// 定义一个部分类用于标记自动发现命令处理器 -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; +1. `[Command]` 特性支持多个单词,表示子命令,如 `[Command("remote add")]`。 +1. 没有标 `[Command]` 特性,或标了但传 `null` 或空字符串时,表示默认命令,如 `[Command("")]`。 +1. 如果多个命令处理器匹配同一个命令,会抛出 `CommandNameAmbiguityException`。 +1. 命令处理器中,有任何一个是异步时,你将必须使用 `RunAsync` 替代 `Run`,否则会编译不通过。 -// 在程序入口添加所有命令处理器 -var commandLine = CommandLine.Parse(args); -commandLine.AddHandlers() - .Run(); -``` +## URL协议支持 -通常,处理器类需要添加 `[Command]` 特性并实现 `ICommandHandler` 接口,它就会被自动发现和添加: +DotNetCampus.CommandLine 支持解析 URL 协议字符串,格式如下: -```csharp -[Command("sample")] -internal class SampleCommandHandler : ICommandHandler -{ - [Option("SampleProperty")] - public required string Option { get; init; } +```ini +// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 +``` - [Value(Length = int.MaxValue)] - public string? Argument { get; init; } +本文开头示例中的那个命令行,使用 URL 传入的话将是下面这样: - public Task RunAsync() - { - // 实现命令处理逻辑 - return Task.FromResult(0); - } -} +```ini +# `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` +dotnet-campus://1.txt/2.txt?count=20&test-name=BenchmarkTest&detail-level=High&debug ``` -此外,你也可以创建一个没有 `[Command]` 特性的命令处理器作为默认处理器。在程序集中最多只能有一个没有 `[Command]` 特性的命令处理器,它将在没有其他命令匹配时被使用: +特别的: -```csharp -// 没有 [Command] 特性的默认处理器 -internal class DefaultCommandHandler : ICommandHandler -{ - [Option('h', "help")] - public bool ShowHelp { get; init; } +1. 集合类型选项可通过重复参数名传入多个值,如:`tags=csharp&tags=dotnet` +2. URL中的特殊字符和非 ASCII 字符会自动进行 URL 解码 - public Task RunAsync() - { - // 处理默认命令,如显示帮助信息等 - if (ShowHelp) - { - Console.WriteLine("显示帮助信息..."); - } - return Task.FromResult(0); - } -} -``` +## 源生成器、拦截器与性能优化 -这种方式特别适合大型应用或扩展性强的命令行工具,可以在不修改入口代码的情况下添加新命令。 +DotNetCampus.CommandLine 使用源代码生成器技术大幅提升了命令行解析的性能。其中的拦截器([Interceptor](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md))让性能提升发挥得更淋漓尽致。 -### 异步命令处理 +### 源生成器生成的代码示例 -对于需要异步执行的命令处理,可以使用`RunAsync`方法: +下面是一个简单的命令行选项类型及其对应生成的源代码示例: ```csharp -await commandLine.AddHandler(async options => +// 用户代码中的类型 +public class BenchmarkOptions41 { - await ImportDataAsync(options); - return 0; -}) -.RunAsync(); -``` + [Option("debug")] + public required bool IsDebugMode { get; init; } -## URL协议支持 + [Option('c', "count")] + public required int TestCount { get; init; } -DotNetCampus.CommandLine 支持解析 URL 协议字符串: + [Option('n', "test-name")] + public string? TestName { get; set; } -``` -dotnet-campus://open/document.txt?readOnly=true&mode=Display&silence=true&startup-sessions=89EA9D26-6464-4E71-BD04-AA6516063D83 -``` + [Option("test-category")] + public string? TestCategory { get; set; } -URL协议解析的特点和用法: + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; -1. URL路径部分(如示例中的 `open/document.txt`)会被解析为位置参数或命令加位置参数 - - 路径的第一部分可作为命令(需标记 `[Command]` 特性) - - 随后的路径部分会被解析为位置参数 -2. 查询参数(`?` 后的部分)会被解析为命名选项 -3. 集合类型选项可通过重复参数名传入多个值,如:`tags=csharp&tags=dotnet` -4. URL中的特殊字符和非ASCII字符会自动进行URL解码 - -## 命名约定与最佳实践 - -为确保更好的兼容性和用户体验,我们建议使用 kebab-case 风格命名长选项: - -```csharp -// 推荐 -[Option('o', "output-file")] -public string OutputFile { get; init; } - -// 不推荐 -[Option('o', "OutputFile")] -public string OutputFile { get; init; } + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} ``` -使用kebab-case命名的好处: - -1. 提供更清晰的单词分割信息(如能猜出"DotNet-Campus"而不是"Dot-Net-Campus") -2. 解决数字从属问题(如"Version2Info"是"Version2-Info"还是"Version-2-Info") -3. 与多种命令行风格更好地兼容 - -## 源生成器、拦截器与性能优化 - -DotNetCampus.CommandLine 使用源代码生成器技术大幅提升了命令行解析的性能。其中的拦截器([Interceptor](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md))让性能提升发挥得更淋漓尽致。 - -### 拦截器的工作原理 - -当你调用 `CommandLine.As()` 或 `CommandLine.AddHandler()` 等方法时,源生成器会自动生成拦截代码,将调用重定向到编译时生成的高性能代码路径。这使得命令行参数解析和对象创建的性能得到了大幅提升。 - -例如,当你编写以下代码时: +对应生成的源: ```csharp -var options = CommandLine.Parse(args).As(); -``` +#nullable enable +using global::System; +using global::DotNetCampus.Cli.Compiler; -源生成器会拦截这个调用,自动生成类似以下的代码来替代默认通过字典查找创建器的方式实现(旧版本曾使用过反射): +namespace DotNetCampus.Cli.Performance.Fakes; -```csharp ///

-/// 方法的拦截器。拦截以提高性能。 +/// 辅助 生成命令行选项、子命令或处理函数的创建。 /// -[global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xInterceptor @Program.cs */ "G4GJAK7udHFnPkRUqV6VzzgRAABQcm9ncmFtLmNz")] -public static T CommandLine_As_DotNetCampusCliTestsFakesOptions(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : global::DotNetCampus.Cli.Tests.Fakes.Options +public sealed class BenchmarkOptions41Builder(global::DotNetCampus.Cli.CommandLine commandLine) { - return (T)global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance(commandLine); -} -``` + public static global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) + { + return new DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41Builder(commandLine).Build(); + } -### 源生成器生成的代码示例 + private global::DotNetCampus.Cli.Compiler.BooleanArgument IsDebugMode = new(); -下面是一个简单的命令行选项类型及其对应生成的源代码示例: + private global::DotNetCampus.Cli.Compiler.NumberArgument TestCount = new(); -```csharp -// 用户代码中的类型 -internal record DotNet03_MixedOptions -{ - [Option] - public int Number { get; init; } + private global::DotNetCampus.Cli.Compiler.StringArgument TestName = new(); - [Option] - public required string Text { get; init; } + private global::DotNetCampus.Cli.Compiler.StringArgument TestCategory = new(); - [Option] - public bool Flag { get; init; } -} -``` - -对应生成的源: + private __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ DetailLevel = new(); -```csharp -#nullable enable -namespace DotNetCampus.Cli.Tests; + private global::DotNetCampus.Cli.Compiler.StringListArgument TestItems = new(); -/// -/// 辅助 生成命令行选项、命令或处理函数的创建。 -/// -internal sealed class DotNet03_MixedOptionsBuilder -{ - public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) + public global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 Build() { - var caseSensitive = commandLine.DefaultCaseSensitive; - var result = new global::DotNetCampus.Cli.Tests.DotNet03_MixedOptions + if (commandLine.RawArguments.Count is 0) { - Number = commandLine.GetOption("number") ?? default, - Text = commandLine.GetOption("text") ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option '--text'. Command line: {commandLine}", "Text"), - Flag = commandLine.GetOption("flag") ?? default, - // There is no positional argument to be initialized. + return BuildDefault(); + } + + var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(commandLine, "BenchmarkOptions41", 0) + { + MatchLongOption = MatchLongOption, + MatchShortOption = MatchShortOption, + MatchPositionalArguments = MatchPositionalArguments, + AssignPropertyValue = AssignPropertyValue, }; - // There is no option to be assigned. - // There is no positional argument to be assigned. - return result; + parser.Parse().ThrowIfError(); + return BuildCore(commandLine); } -} -``` -代码中的方法调用: + private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, global::DotNetCampus.Cli.CommandNamingPolicy namingPolicy) + { + // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。 + switch (longOption) + { + case "debug": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); + case "count": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); + case "test-name": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); + case "test-category": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); + case "detail-level": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); + } -```csharp -_ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); -``` + var defaultComparison = defaultCaseSensitive + ? global::System.StringComparison.Ordinal + : global::System.StringComparison.OrdinalIgnoreCase; -对应生成的源(拦截器): + // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。 + if (namingPolicy.SupportsOrdinal()) + { + if (longOption.Equals("debug".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); + } + if (longOption.Equals("count".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("test-name".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("test-category".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("detail-level".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); + } + } -```csharp -#nullable enable + // 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。 + if (namingPolicy.SupportsPascalCase()) + { + if (longOption.Equals("Debug".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); + } + if (longOption.Equals("Count".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("TestName".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("TestCategory".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (longOption.Equals("DetailLevel".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); + } + } -namespace DotNetCampus.Cli.Compiler -{ - file static class Interceptors + return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; + } + + private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) { - /// - /// 方法的拦截器。拦截以提高性能。 - /// - [global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xInterceptor @Program.cs */ "G4GJAK7udHFnPkRUqV6VzzgRAABQcm9ncmFtLmNz")] - [global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xModule @Program.cs */ "G4GJAK7udHFnPkRUqV6VzxkSAABQcm9ncmFtLmNz")] - public static T CommandLine_As_DotNetCampusCliTestsFakesOptions(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : global::DotNetCampus.Cli.Tests.Fakes.Options + // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。 + switch (shortOption) + { + // 属性 IsDebugMode 没有短名称,无需匹配。 + case "c": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); + case "n": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); + // 属性 TestCategory 没有短名称,无需匹配。 + case "d": + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); + } + + var defaultComparison = defaultCaseSensitive + ? global::System.StringComparison.Ordinal + : global::System.StringComparison.OrdinalIgnoreCase; + + // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。 + // 属性 IsDebugMode 没有短名称,无需匹配。 + if (shortOption.Equals("c".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); + } + if (shortOption.Equals("n".AsSpan(), defaultComparison)) { - return (T)global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance(commandLine); + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); } + // 属性 TestCategory 没有短名称,无需匹配。 + if (shortOption.Equals("d".AsSpan(), defaultComparison)) + { + return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); + } + + return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; } -} -namespace System.Runtime.CompilerServices -{ - [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : global::System.Attribute + private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) { - public InterceptsLocationAttribute(int version, string data) + // 属性 TestItems 覆盖了所有位置参数,直接匹配。 + return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("TestItems", 5, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); + } + + private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) + { + switch (propertyIndex) { - _ = version; - _ = data; + case 0: + IsDebugMode = IsDebugMode.Assign(value); + break; + case 1: + TestCount = TestCount.Assign(value); + break; + case 2: + TestName = TestName.Assign(value); + break; + case 3: + TestCategory = TestCategory.Assign(value); + break; + case 4: + DetailLevel = DetailLevel.Assign(value); + break; + case 5: + TestItems = TestItems.Append(value); + break; } } -} -``` -代码中的程序集命令处理器搜集: + private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 BuildCore(global::DotNetCampus.Cli.CommandLine commandLine) + { + var result = new global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 + { + // 1. There is no [RawArguments] property to be initialized. -```csharp -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; -``` + // 2. [Option] + IsDebugMode = IsDebugMode.ToBoolean() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'debug'. Command line: {commandLine}", "IsDebugMode"), + TestCount = TestCount.ToInt32() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'count'. Command line: {commandLine}", "TestCount"), -对应生成的源: + // 3. [Value] + TestItems = TestItems.ToList(), + }; -```csharp -#nullable enable -namespace DotNetCampus.Cli.Tests.Fakes; + // 1. There is no [RawArguments] property to be assigned. -/// -/// 提供一种辅助自动搜集并执行本程序集中所有命令行处理器的方式。 -/// -partial class AssemblyCommandHandler : global::DotNetCampus.Cli.Compiler.ICommandHandlerCollection -{ - public global::DotNetCampus.Cli.ICommandHandler? TryMatch(string? command, global::DotNetCampus.Cli.CommandLine cl) => command switch + // 2. [Option] + if (TestName.ToString() is { } o0) + { + result.TestName = o0; + } + if (TestCategory.ToString() is { } o1) + { + result.TestCategory = o1; + } + if (DetailLevel.ToDetailLevel() is { } o2) + { + result.DetailLevel = o2; + } + + // 3. There is no [Value] property to be assigned. + + return result; + } + + private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 BuildDefault() { - null => throw new global::DotNetCampus.Cli.Exceptions.CommandVerbAmbiguityException($"Multiple command handlers match the same command name 'null': AmbiguousOptions, CollectionOptions, ComparedOptions, DefaultCommandHandler, DictionaryOptions, FakeCommandOptions, Options, PrimaryOptions, UnlimitedValueOptions, ValueOptions.", null), - // 类型 EditOptions 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 - "Fake" => (global::DotNetCampus.Cli.ICommandHandler)global::DotNetCampus.Cli.Tests.Fakes.FakeCommandHandlerBuilder.CreateInstance(cl), - // 类型 PrintOptions 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 - // 类型 ShareOptions 没有继承 ICommandHandler 接口,因此无法统一调度执行,只能由开发者单独调用。 - _ => null, - }; + throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!); + } + + /// + /// Provides parsing and assignment for the enum type . + /// + private readonly record struct __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ + { + /// + /// Indicates whether to ignore exceptions when parsing fails. + /// + public bool IgnoreExceptions { get; init; } + + /// + /// Stores the parsed enum value. + /// + private global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? Value { get; init; } + + /// + /// Assigns a value when a command line input is parsed. + /// + /// The parsed string value. + public __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ Assign(ReadOnlySpan value) + { + Span lowerValue = stackalloc char[value.Length]; + for (var i = 0; i < value.Length; i++) + { + lowerValue[i] = char.ToLowerInvariant(value[i]); + } + global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? newValue = lowerValue switch + { + "low" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Low, + "medium" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Medium, + "high" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.High, + _ when IgnoreExceptions => null, + _ => throw new global::DotNetCampus.Cli.Exceptions.CommandLineParseValueException($"Cannot convert '{value.ToString()}' to enum type 'DotNetCampus.Cli.Performance.Fakes.DetailLevel'."), + }; + return this with { Value = newValue }; + } + + /// + /// Converts the parsed value to the enum type. + /// + public global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? ToDetailLevel() => Value; + } } ``` @@ -579,47 +618,23 @@ partial class AssemblyCommandHandler : global::DotNetCampus.Cli.Compiler.IComman 源代码生成器实现提供了极高的命令行解析性能: -| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | -|---------------------------------------- |----------------:|--------------:|--------------:|----------------:|-------:|-------:|----------:| -| 'parse [] --flexible' | 39.16 ns | 0.402 ns | 0.357 ns | 39.15 ns | 0.0124 | - | 208 B | -| 'parse [] --gnu' | 38.22 ns | 0.518 ns | 0.459 ns | 38.30 ns | 0.0124 | - | 208 B | -| 'parse [] --posix' | 38.45 ns | 0.792 ns | 0.741 ns | 38.45 ns | 0.0124 | - | 208 B | -| 'parse [] --dotnet' | 42.14 ns | 0.878 ns | 2.588 ns | 42.06 ns | 0.0124 | - | 208 B | -| 'parse [] --powershell' | 38.67 ns | 0.772 ns | 1.451 ns | 38.42 ns | 0.0124 | - | 208 B | -| 'parse [] -v=3.x -p=parser' | 44.07 ns | 0.665 ns | 0.841 ns | 44.08 ns | 0.0220 | - | 368 B | -| 'parse [] -v=3.x -p=runtime' | 365.36 ns | 7.186 ns | 13.319 ns | 361.47 ns | 0.0367 | - | 616 B | -| 'parse [PS1] --flexible' | 907.15 ns | 17.887 ns | 38.504 ns | 899.46 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] --dotnet' | 969.51 ns | 18.977 ns | 31.179 ns | 964.56 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] -v=3.x -p=parser' | 448.38 ns | 8.883 ns | 13.830 ns | 445.91 ns | 0.0715 | - | 1200 B | -| 'parse [PS1] -v=3.x -p=runtime' | 835.83 ns | 16.055 ns | 38.774 ns | 830.59 ns | 0.0858 | - | 1448 B | -| 'parse [CMD] --flexible' | 932.31 ns | 18.636 ns | 40.907 ns | 936.14 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] --dotnet' | 877.96 ns | 8.846 ns | 9.832 ns | 877.67 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] -v=3.x -p=parser' | 438.09 ns | 8.591 ns | 11.469 ns | 433.77 ns | 0.0715 | - | 1200 B | -| 'parse [CMD] -v=3.x -p=runtime' | 822.05 ns | 16.417 ns | 25.560 ns | 811.08 ns | 0.0858 | - | 1448 B | -| 'parse [GNU] --flexible' | 880.14 ns | 17.627 ns | 36.794 ns | 878.35 ns | 0.1574 | - | 2648 B | -| 'parse [GNU] --gnu' | 811.59 ns | 13.691 ns | 20.492 ns | 805.61 ns | 0.1554 | - | 2608 B | -| 'parse [GNU] -v=3.x -p=parser' | 492.48 ns | 9.757 ns | 11.615 ns | 491.95 ns | 0.0896 | - | 1512 B | -| 'parse [GNU] -v=3.x -p=runtime' | 873.40 ns | 15.873 ns | 24.713 ns | 865.86 ns | 0.1049 | - | 1760 B | -| 'handle [Edit,Print] --flexible' | 693.30 ns | 13.894 ns | 28.066 ns | 681.77 ns | 0.2375 | 0.0019 | 3984 B | -| 'handle [Edit,Print] -v=3.x -p=parser' | 949.15 ns | 18.959 ns | 25.952 ns | 939.97 ns | 0.2775 | 0.0038 | 4648 B | -| 'handle [Edit,Print] -v=3.x -p=runtime' | 6,232.90 ns | 122.601 ns | 217.924 ns | 6,190.80 ns | 0.2594 | - | 4592 B | -| 'parse [URL]' | 2,942.05 ns | 54.322 ns | 76.152 ns | 2,926.04 ns | 0.4578 | - | 7704 B | -| 'parse [URL] -v=3.x -p=parser' | 121.43 ns | 2.457 ns | 5.496 ns | 121.10 ns | 0.0440 | - | 736 B | -| 'parse [URL] -v=3.x -p=runtime' | 462.92 ns | 9.017 ns | 10.023 ns | 464.26 ns | 0.0587 | - | 984 B | -| 'NuGet: CommandLineParser' | 212,745.53 ns | 4,237.822 ns | 11,384.635 ns | 211,418.82 ns | 5.3711 | - | 90696 B | -| 'NuGet: System.CommandLine' | 1,751,023.59 ns | 34,134.634 ns | 50,034.108 ns | 1,727,339.45 ns | 3.9063 | - | 84138 B | +*数据等待生成中* 其中: + 1. `parse` 表示调用的是 `CommandLine.Parse` 方法 -2. `handle` 表示调用的是 `CommandLine.AddHandler` 方法 -3. 中括号 `[Xxx]` 表示传入的命令行参数的风格 -4. `--flexible` `--gnu` 等表示解析传入命令行时所使用的解析器风格(相匹配时效率最高) -5. `-v=3.x -p=parser` 表示旧版本手工编写解析器并传入时的性能(性能最好,不过旧版本支持的命令行规范较少,很多合法的命令写法并不支持) -6. `-v=3.x -p=runtime` 表示旧版本使用默认的反射解析器时的性能 -7. `NuGet: CommandLineParser` 和 `NuGet: System.CommandLine` 表示使用对应名称的 NuGet 包解析命令行参数时的性能 -8. `parse [URL]` 表示解析 URL 协议字符串时的性能 - -新版本得益于源生成器和拦截器: -1. 完成一次解析大约在 0.8μs(微秒)左右(Benchmark) -2. 在应用程序启动期间,完成一次解析只需要大约 34μs -3. 在应用程序启动期间,包含dll加载、类型初始化在内的解析一次大约8ms(使用 AOT 编译能重新降至 34μs)。 +1. `handle` 表示调用的是 `CommandLine.AddHandler` 方法 +1. 中括号 `[Xxx]` 表示传入的命令行参数的风格 +1. `--flexible` `--gnu` 等表示解析传入命令行时所使用的解析器风格(相匹配时效率最高) +1. `-v=3.x -p=parser` 表示旧版本手工编写解析器并传入时的性能(性能最好,不过旧版本支持的命令行规范较少,很多合法的命令写法并不支持) +1. `-v=3.x -p=runtime` 表示旧版本使用默认的反射解析器时的性能 +1. `-v=4.0 -p=dotnet` 表示数月前的 4.0 预览版的性能 +1. `-v=4.1 -p=dotnet` 表示当前版本的性能 +1. `NuGet: ConsoleAppFramework`、`NuGet: CommandLineParser` 和 `NuGet: System.CommandLine` 表示使用对应名称的 NuGet 包解析命令行参数时的性能 +1. `parse [URL]` 表示解析 URL 协议字符串时的性能 + +库作者 @walterlv 的感受: + +1. 性能最好的是 [ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework) 库,我们的 [DotNetCampus.CommandLine](https://github.com/dotnet-campus/DotNetCampus.CommandLine) 比它差一点,不过仍然在同一个数量级。对比其他库,我们俩比他们好了几个数量级。 +1. 非常感谢 [ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework) 的极致追求(零依赖、零开销、零反射、零分配,由 C# 源码生成器提供的 AOT 安全 CLI 框架)。虽然在发现它之前我们就已经在使用源生成器和拦截器了(`-v4.0`),但确实是它让我们看到了更高的目标和动力,写了现在的版本(`-v4.1`)。 +1. 当然,[ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework) 的目标是极致的性能追求,为此确实也牺牲了一部分命令行语法支持;而我们的目标是在「全功能」的基础上实现极致的性能追求,所以性能最多只能打在同一级别,确实也无法超越它。如果你的程序极致追求性能,并且使用人群倾向于专业人士或应用程序,则非常推荐使用它;不过如果你希望你的程序极致追求性能的同时,也面向大众群体(非专业人士)或各种不同喜好的人群体,则非常推荐使用我们 [DotNetCampus.CommandLine](https://github.com/dotnet-campus/DotNetCampus.CommandLine)。 From 0600c57bea692aac267908a9766cfaf00df2aa3a Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 20:36:39 +0800 Subject: [PATCH 073/193] =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-hans/README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index 4b69a5a1..31e64b9a 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -616,9 +616,41 @@ public sealed class BenchmarkOptions41Builder(global::DotNetCampus.Cli.CommandLi ## 性能数据 -源代码生成器实现提供了极高的命令行解析性能: +源代码生成器实现提供了极高的命令行解析性能。 -*数据等待生成中* +解析空白命令行参数: + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------------ |-------------:|-----------:|-----------:|-------:|----------:| +| 'parse [] -v=4.1 -p=flexible' | 27.25 ns | 0.485 ns | 0.454 ns | 0.0143 | 240 B | +| 'parse [] -v=4.1 -p=dotnet' | 27.35 ns | 0.471 ns | 0.440 ns | 0.0143 | 240 B | +| 'parse [] -v=4.0 -p=flexible' | 97.16 ns | 0.708 ns | 0.628 ns | 0.0134 | 224 B | +| 'parse [] -v=4.0 -p=dotnet' | 95.90 ns | 0.889 ns | 0.742 ns | 0.0134 | 224 B | +| 'parse [] -v=3.x -p=parser' | 49.73 ns | 0.931 ns | 0.870 ns | 0.0239 | 400 B | +| 'parse [] -v=3.x -p=runtime' | 19,304.17 ns | 194.337 ns | 162.280 ns | 0.4272 | 7265 B | + +解析 GNU 风格命令行参数: + +```bash +test DotNetCampus.CommandLine.Performance.dll DotNetCampus.CommandLine.Sample.dll DotNetCampus.CommandLine.Test.dll -c 20 --test-name BenchmarkTest --detail-level High --debug +``` + +| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated | +|--------------------------------- |-------------- |-------------- |------------:|----------:|----------:|-------:|----------:| +| 'parse [GNU] -v=4.1 -p=flexible' | .NET 10.0 | .NET 10.0 | 355.9 ns | 4.89 ns | 4.58 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.1 -p=gnu' | .NET 10.0 | .NET 10.0 | 339.7 ns | 6.81 ns | 7.57 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.0 -p=flexible' | .NET 10.0 | .NET 10.0 | 945.9 ns | 14.87 ns | 13.19 ns | 0.1583 | 2656 B | +| 'parse [GNU] -v=4.0 -p=gnu' | .NET 10.0 | .NET 10.0 | 882.1 ns | 11.30 ns | 10.57 ns | 0.1631 | 2736 B | +| 'parse [GNU] -v=3.x -p=parser' | .NET 10.0 | .NET 10.0 | 495.7 ns | 9.26 ns | 9.09 ns | 0.1040 | 1752 B | +| 'parse [GNU] -v=3.x -p=runtime' | .NET 10.0 | .NET 10.0 | 18,025.5 ns | 194.73 ns | 162.61 ns | 0.4883 | 8730 B | +| 'NuGet: ConsoleAppFramework' | .NET 10.0 | .NET 10.0 | 134.1 ns | 2.70 ns | 2.65 ns | 0.0215 | 360 B | +| 'parse [GNU] -v=4.1 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 624.3 ns | 7.06 ns | 6.60 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.1 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 600.3 ns | 6.72 ns | 6.28 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.0 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 1,395.6 ns | 20.43 ns | 19.11 ns | 0.1507 | 2529 B | +| 'parse [GNU] -v=4.0 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 1,438.1 ns | 19.84 ns | 18.55 ns | 0.1545 | 2609 B | +| 'parse [GNU] -v=3.x -p=parser' | NativeAOT 9.0 | NativeAOT 9.0 | 720.8 ns | 7.47 ns | 6.99 ns | 0.1030 | 1737 B | +| 'parse [GNU] -v=3.x -p=runtime' | NativeAOT 9.0 | NativeAOT 9.0 | NA | NA | NA | NA | NA | +| 'NuGet: ConsoleAppFramework' | NativeAOT 9.0 | NativeAOT 9.0 | 195.3 ns | 3.76 ns | 3.69 ns | 0.0234 | 392 B | 其中: From e95510c0f99dadcd0fc820c8c46f3ed98acb62fd Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 20:56:10 +0800 Subject: [PATCH 074/193] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20AI=20=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 +- docs/en/README.md | 697 ++++++++++++++--------------------------- docs/zh-hans/README.md | 273 +--------------- docs/zh-hant/README.md | 635 ++++++++++++++----------------------- 4 files changed, 477 insertions(+), 1155 deletions(-) diff --git a/README.md b/README.md index 7f05ae3f..276cf179 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ [zh-hans]: /docs/zh-hans/README.md [zh-hant]: /docs/zh-hant/README.md -DotNetCampus.CommandLine is a simple yet high-performance command line parsing library for .NET. Thanks to the power of source code generators, it provides efficient parsing capabilities with a developer-friendly experience. +DotNetCampus.CommandLine is a simple yet high-performance command line parsing library for .NET. Powered by source generators (and interceptors), it delivers efficient parsing and a friendly development experience across multiple command line styles. -Parsing a typical command line takes only about 0.8μs (microseconds), making it one of the fastest command line parsers available in .NET. +Parsing a typical command line takes only about 0.8μs (microseconds) in benchmarks, placing it among the fastest .NET command line parsers while still pursuing full-featured syntax support. ## Get Started @@ -80,20 +80,15 @@ $ demo.exe "C:/Users/lvyi/Desktop/demo.txt" -s --mode Edit --startup-sessions A ## Command Styles and Features -The library supports multiple command line styles through `CommandLineStyle` enum: -- Flexible (default): Intelligently recognizes multiple styles -- GNU: GNU standard compliant -- POSIX: POSIX standard compliant -- DotNet: .NET CLI style -- PowerShell: PowerShell style - -Advanced features include: -- Support for various data types including collections and dictionaries -- Positional arguments with `ValueAttribute` -- Required properties with C# `required` modifier -- Command handling with command support -- URL protocol parsing -- High performance thanks to source generators +The library supports multiple command line styles via `CommandLineStyle` (Flexible/DotNet/Gnu/Posix/PowerShell) and a Flexible default that offers broad compatibility (case sensitivity depends on style). Key capabilities include: +- Rich option syntax (long/short options, multiple separators `= : space`) +- Booleans with multiple literal forms (true/false, yes/no, on/off, 1/0) +- Collections (repeat, comma, semicolon, space forms) and dictionaries +- Positional arguments via `ValueAttribute` (ranges allowed) +- Required / nullable / immutable (`required` / `init`) property semantics +- Command & subcommand handling (multi-word `[Command]` supported) +- Optional URL protocol parsing (`schema://...` form) +- High performance from source generators and interceptors ## Engage, Contribute and Provide Feedback diff --git a/docs/en/README.md b/docs/en/README.md index 6d1be840..f45cebde 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -1,4 +1,4 @@ -# Command Line Parser +# Command Line Parser | [English][en] | [简体中文][zh-hans] | [繁體中文][zh-hant] | | ------------- | ------------------- | ------------------- | @@ -7,268 +7,219 @@ [zh-hans]: /docs/zh-hans/README.md [zh-hant]: /docs/zh-hant/README.md -DotNetCampus.CommandLine provides a simple yet high-performance command line parsing functionality. Thanks to the power of source code generators, it now offers more efficient parsing capabilities and a more developer-friendly experience. All features are available under the DotNetCampus.Cli namespace. +DotNetCampus.CommandLine provides simple and high-performance command line parsing. Benefiting from source generators (and interceptors), it now delivers more efficient parsing and a friendlier development experience. All features live under the `DotNetCampus.Cli` namespace. -## Quick Start +## Quick Usage ```csharp class Program { static void Main(string[] args) { - // Create a new instance of CommandLine type from command-line arguments + // Create a new CommandLine instance from the command line arguments var commandLine = CommandLine.Parse(args); - // Parse the command line into an instance of Options type - // Source generator will automatically handle the parsing process for you, no need to manually create a parser + // Parse the command line into an instance of the Options type + // The source generator automatically performs the parsing; no manual parser creation needed var options = commandLine.As(); - // Next, write your other functionality using your options object + // Next, use your options object to implement other functionality } } ``` -You need to define a type that maps to command-line parameters: +You need to define a type that contains the mapping for command line arguments: ```csharp -class Options +public class Options { - [Value(0)] - public required string FilePath { get; init; } - - [Option('s', "silence")] - public bool IsSilence { get; init; } - - [Option('m', "mode")] - public string? StartMode { get; init; } + [Option("debug")] + public required bool IsDebugMode { get; init; } - [Option("startup-sessions")] - public IReadOnlyList StartupSessions { get; init; } = []; -} -``` + [Option('c', "count")] + public required int TestCount { get; init; } -Then use different command styles in the command line to populate an instance of this type. The library supports multiple command line styles: + [Option('n', "test-name")] + public string? TestName { get; set; } -### Windows PowerShell Style + [Option("test-category")] + public string? TestCategory { get; set; } -```powershell -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s -Mode Edit -StartupSessions A B C -``` + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; -### Windows CMD Style + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} -```cmd -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" /s /Mode Edit /StartupSessions A B C +public enum DetailLevel +{ + Low, + Medium, + High, +} ``` -### Linux/GNU Style - -```bash -$ demo.exe "C:/Users/lvyi/Desktop/demo.txt" -s --mode Edit --startup-sessions A --startup-sessions B --startup-sessions C -``` +Then use different styles of command lines to populate an instance. The library supports multiple styles: -### .NET CLI Style -``` -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s:true --mode:Edit --startup-sessions:A;B;C -``` +| Style | Example | +| ---------- | ------------------------------------------------------------------------------------------ | +| DotNet | `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` | +| PowerShell | `demo.exe 1.txt 2.txt 3.txt -c 20 -TestName BenchmarkTest -DetailLevel High -Debug` | +| CMD | `demo.exe 1.txt 2.txt 3.txt /c 20 /TestName BenchmarkTest /DetailLevel High /Debug` | +| Gnu | `demo.exe 1.txt 2.txt 3.txt -c 20 --test-name BenchmarkTest --detail-level High --debug` | +| Flexible | `demo.exe 1.txt 2.txt 3.txt --count:20 /TestName BenchmarkTest --detail-level=High -Debug` | ## Command Line Styles -DotNetCampus.CommandLine supports multiple command line styles, and you can specify which style to use during parsing: +DotNetCampus.CommandLine supports multiple styles; you can specify one when parsing: ```csharp -// Use .NET CLI style to parse command-line arguments +// Parse using the .NET CLI style var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); ``` Supported styles include: -- `CommandLineStyle.Flexible` (default): Smartly recognizes multiple styles, case-insensitive by default, and is an effective combination of DotNet/GNU/PowerShell styles - - Supports all styles shown in the previous examples and can correctly parse them - - Fully supports all command-line features of the DotNet style (including lists and dictionaries) - - Supports all features of the GNU style except short name parameters (e.g., `-o1.txt`) and short name abbreviations (e.g., `-abc` represents `-a -b -c`) - - Due to strict Posix rules, Flexible style naturally supports Posix style - - The DotNet style itself is compatible with PowerShell command line style, so Flexible style also supports PowerShell style -- `CommandLineStyle.Gnu`: Style conforming to the GNU specification, case-sensitive by default -- `CommandLineStyle.Posix`: Style conforming to the POSIX specification, case-sensitive by default -- `CommandLineStyle.DotNet`: .NET CLI style, case-insensitive by default -- `CommandLineStyle.PowerShell`: PowerShell style, case-insensitive by default - -## Data Type Support - -The library supports parsing of multiple data types: - -1. **Basic Types**: Strings, integers, booleans, enums, etc. -2. **Collection Types**: Arrays, lists, read-only collections, immutable collections -3. **Dictionary Types**: IDictionary, IReadOnlyDictionary, ImmutableDictionary, etc. - -### Boolean Type Options - -For boolean type options, there are multiple ways to specify them in the command line: - -- Specifying only the option name indicates `true`: `-s` or `--silence` -- Explicitly specify a value: `-s:true`, `-s=false`, `--silence:on`, `--silence=off` - -### Collection Type Options - -For collection type options, you can specify the same option multiple times, or use semicolons to separate multiple values: - -``` -demo.exe --files file1.txt --files file2.txt -demo.exe --files:file1.txt;file2.txt;file3.txt -``` - -### Dictionary Type Options - -For dictionary type options, multiple input methods are supported: - -``` -demo.exe --properties key1=value1 --properties key2=value2 -demo.exe --properties:key1=value1;key2=value2 -``` - -## Positional Arguments - -In addition to named options, you can also use positional arguments, specifying the position of the arguments using the `ValueAttribute`: +- `CommandLineStyle.Flexible` (default): Flexible style offering broad compatibility among styles; case-insensitive by default +- `CommandLineStyle.DotNet`: .NET CLI style; case-sensitive by default +- `CommandLineStyle.Gnu`: GNU-compliant style; case-sensitive by default +- `CommandLineStyle.Posix`: POSIX-compliant style; case-sensitive by default +- `CommandLineStyle.PowerShell`: PowerShell style; case-insensitive by default + +By default, their detailed differences are: + +| Style | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ----------------------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ----------------- | +| Case | Insensitive | Sensitive | Sensitive | Sensitive | Insensitive | Insensitive | +| Long options | Supported | Supported | Supported | Not supported | Supported | Supported | +| Short options | Supported | Supported | Supported | Supported | Supported | Not supported | +| Option value `=` | -o=value | -o=value | -o=value | | | option=value | +| Option value `:` | -o:value | -o:value | | | | | +| Option value (space) | -o value | -o value | -o value | -o value | -o value | | +| Boolean option (implicit true) | -o | -o | -o | -o | -o | option | +| Boolean option (with value) | -o=true | -o=true | | | -o:true | option=true | +| Boolean values | true/false | true/false | true/false | true/false | true/false | true/false | +| Boolean values | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| Boolean values | on/off | on/off | on/off | on/off | on/off | on/off | +| Boolean values | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| Collection option | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| Collection option `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | +| Collection option `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | +| Collection option (space separated) | -o A B C | -o A B C | | | -o A B C | | +| Dictionary option | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| Combined short booleans | Not supported | Not supported | -abc | -abc | Not supported | Not supported | +| Single short option multi chars | -ab | -ab | Not supported | Not supported | -ab | Not supported | +| Short option directly with value | Not supported | Not supported | -o1.txt | Not supported | Not supported | Not supported | +| Long option prefixes | `--` `-` `/` | `--` | `--` | (None) | `-` `/` | | +| Short option prefixes | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| Naming | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| Naming | -PascalCase | | | | -PascalCase | | +| Naming | -camelCase | | | | -camelCase | | +| Naming | /PascalCase | | | | /PascalCase | | +| Naming | /camelCase | | | | /camelCase | | + +## Naming + +1. When defining an option in code, you should use kebab-case + - [Why do this?](https://github.com/dotnet-campus/DotNetCampus.CommandLine/blob/main/docs/analyzers/DCL101.md) + - If we suspect you did not use kebab-case, we'll emit warning DCL101 + - You may ignore the warning; regardless of the string you write, we treat it as kebab-case (this provides unambiguous word boundary info; see example) +2. After you define a string treated as kebab-case + - Depending on the style you set, you can use any of kebab-case, PascalCase, and camelCase + +Example command line type: ```csharp -class FileOptions +[Command("open command-line")] +public class Options { - [Value(0)] - public string InputFile { get; init; } - - [Value(1)] - public string OutputFile { get; init; } - - [Option('v', "verbose")] - public bool Verbose { get; init; } + [Option('o', "option-name")] + public required string OptionName { get; init; } } ``` -Usage: +Two kebab-case usages here: the `Command` attribute and the `Option` attribute. You can accept: -``` -demo.exe input.txt output.txt --verbose -``` +- DotNet/Gnu style: `demo.exe open command-line --option-name value` +- PowerShell style: `demo.exe Open CommandLine -OptionName value` +- CMD style: `demo.exe Open CommandLine /optionName value` -You can also capture multiple positional arguments into an array or collection: +If you instead write them in other styles, you might get results different from expectations (or maybe intentional): ```csharp -class MultiFileOptions +#pragma warning disable DCL101 +[Command("Open CommandLine")] +public class Options { - [Value(0, Length = int.MaxValue)] - public string[] Files { get; init; } = []; + // Analyzer warning: OptionName is not kebab-case. Suppress DCL101 if desired. + [Option('o', "OptionName")] + public required string OptionName { get; init; } } +#pragma warning restore DCL101 ``` -## Combining Options and Positional Arguments +Because we treat them as kebab-case anyway, you will accept: -`ValueAttribute` and `OptionAttribute` can be applied to the same property simultaneously: +- DotNet/Gnu style: `demo.exe Open CommandLine --OptionName value` +- PowerShell style: `demo.exe Open CommandLine -OptionName value` +- CMD style: `demo.exe Open CommandLine /optionName value` -```csharp -class Options -{ - [Value(0), Option('f', "file")] - public string FilePath { get; init; } -} -``` +## Data Types -This way, all of the following command lines will assign the file path to the `FilePath` property: +The library supports many data types: -``` -demo.exe file.txt -demo.exe -f file.txt -demo.exe --file file.txt -``` +1. **Basic types**: string, integer, boolean, enum, etc. +2. **Collection types**: arrays, lists, read-only collections, immutable collections +3. **Dictionary types**: `IDictionary`, `IReadOnlyDictionary`, `ImmutableDictionary`, etc. -## Required and Optional Options +See the big table above for how these are passed on the command line. -In C# 11 and above, you can use the `required` modifier to mark required options: +## Required Options and Default Values -```csharp -class Options -{ - [Option('i', "input")] - public required string InputFile { get; init; } // Required option - - [Option('o', "output")] - public string? OutputFile { get; init; } // Optional option -} -``` +When defining a property, these modifiers apply: -If a required option is not provided, a `RequiredPropertyNotAssignedException` exception will be thrown during parsing. +1. Use `required` to mark that an option is mandatory +2. Use `init` to mark that an option is immutable +3. Use `?` to mark that an option is nullable -## Property Initial Values and Accessor Modifiers +What value a property ultimately receives depends on: -When defining option types, you need to be aware of the relationship between property initial values and accessor modifiers (`init`, `required`): +| required | init | Collection | nullable | Behavior | Explanation | +| -------- | ---- | ---------- | -------- | ---------------- | ----------------------------------------------- | +| 1 | _ | _ | _ | Throw | Must be supplied; missing raises exception | +| 0 | 1 | 1 | _ | Empty collection | Collections are never null; missing => empty | +| 0 | 1 | 0 | 1 | null | Nullable; missing => null | +| 0 | 1 | 0 | 0 | Default value | Non-nullable; missing => default(T) | +| 0 | 0 | _ | _ | Keep initial | Not required/immediate; keeps initializer value | -```csharp -class Options -{ - // Incorrect example: When using init or required, default values will be ignored - [Option('f', "format")] - public string Format { get; init; } = "json"; // Default value won't take effect! - - // Correct example: Use set to preserve default values - [Option('f', "format")] - public string Format { get; set; } = "json"; // Default value will be correctly preserved -} -``` - -### Important Notes on Property Initial Values +- 1 = present +- 0 = absent +- _ = regardless -1. **Behavior when using `init` or `required`**: - - When a property includes the `required` or `init` modifier, the property's initial value will be ignored - - If the command-line arguments don't provide a value for this option, the property will be set to `default(T)` (which is `null` for reference types) - - This is determined by C# language features; if the command-line library were to overcome this limitation, it would need to handle all possible combinations of properties, which is obviously very wasteful - -2. **Ways to preserve default values**: - - If you need to provide default values for properties, use `{ get; set; }` instead of `{ get; init; }` - -3. **Nullable types and warning handling**: - - For non-required reference type properties, they should be marked as nullable (e.g., `string?`) to avoid nullable warnings - - For value types (e.g., `int`, `bool`), if you want to preserve the default value rather than `null`, they should not be marked as nullable - -Example: +1. Nullable behavior is the same for reference and value types (default value just yields `null` for reference types) +2. Missing required option throws `RequiredPropertyNotAssignedException` +3. "Keep initial" means you may assign an initial value at definition time: ```csharp -class OptionsBestPractice -{ - // Required option: Use required, no need to worry about default values - [Option("input")] - public required string InputFile { get; init; } - - // Optional option: Mark as nullable type to avoid warnings - [Option("output")] - public string? OutputFile { get; init; } - - // Option that needs default value: Use set instead of init - [Option("format")] - public string Format { get; set; } = "json"; - - // Value type option: No need to mark as nullable - [Option("count")] - public int Count { get; set; } = 1; -} +// Note: Initial value only applies when neither required nor init is used. +[Option('o', "option-name")] +public string OptionName { get; set; } = "Default Value"; ``` -## Command Handling and Commands - -You can use the command handler pattern to handle different commands, similar to `git commit`, `git push`, etc. DotNetCampus.CommandLine provides multiple ways to add command handlers: +## Commands and Subcommands -### 1. Using Delegates to Handle Commands +You can use the command handler pattern to process different commands, like `git commit` or `git remote add`. Multiple ways are provided: -The simplest way is to handle commands through delegates, separating command option types and handling logic: +### 1. Delegate-based handlers ```csharp var commandLine = CommandLine.Parse(args); -commandLine.AddHandler(options => { /* Handle the add command */ }) - .AddHandler(options => { /* Handle the remove command */ }) +commandLine.AddHandler(options => { /* handle add */ }) + .AddHandler(options => { /* handle remove */ }) .Run(); ``` -Use the `Command` attribute to mark commands when defining command option classes: - ```csharp [Command("add")] public class AddOptions @@ -285,9 +236,7 @@ public class RemoveOptions } ``` -### 2. Using the ICommandHandler Interface - -For more complex command handling logic, you can create classes that implement the `ICommandHandler` interface, encapsulating command options and handling logic together: +### 2. `ICommandHandler` interface ```csharp [Command("convert")] @@ -295,331 +244,147 @@ internal class ConvertCommandHandler : ICommandHandler { [Option('i', "input")] public required string InputFile { get; init; } - + [Option('o', "output")] public string? OutputFile { get; init; } - + [Option('f', "format")] public string Format { get; set; } = "json"; - + public Task RunAsync() { - // Implement command handling logic + // Command handling logic Console.WriteLine($"Converting {InputFile} to {Format} format"); // ... - return Task.FromResult(0); // Return exit code + return Task.FromResult(0); // Exit code } } ``` -Then add it directly to the command line parser: - ```csharp var commandLine = CommandLine.Parse(args); -commandLine.AddHandler() +commandLine + .AddHandler() + .AddHandler() + .AddHandler(options => { /* handle remove */ }) .Run(); ``` -### 3. Using Assembly Auto-Discovery of Command Handlers - -For more convenient management of a large number of commands without manually adding each one, you can use the assembly auto-discovery feature to automatically add all classes in the assembly that implement the `ICommandHandler` interface: +### Notes -```csharp -// Define a partial class to mark auto-discovery of command handlers -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; - -// Add all command handlers at the program entry point -var commandLine = CommandLine.Parse(args); -commandLine.AddHandlers() - .Run(); -``` - -Typically, handler classes need to add the `[Command]` attribute and implement the `ICommandHandler` interface, and they will be automatically discovered and added: - -```csharp -[Command("sample")] -internal class SampleCommandHandler : ICommandHandler -{ - [Option("SampleProperty")] - public required string Option { get; init; } - - [Value(Length = int.MaxValue)] - public string? Argument { get; init; } - - public Task RunAsync() - { - // Implement command handling logic - return Task.FromResult(0); - } -} -``` - -Additionally, you can create a command handler without the `[Command]` attribute as the default handler. There can be at most one command handler without the `[Command]` attribute in the assembly, which will be used when no other commands match: - -```csharp -// Default handler without [Command] attribute -internal class DefaultCommandHandler : ICommandHandler -{ - [Option('h', "help")] - public bool ShowHelp { get; init; } - - public Task RunAsync() - { - // Handle default commands, such as displaying help information - if (ShowHelp) - { - Console.WriteLine("Displaying help information..."); - } - return Task.FromResult(0); - } -} -``` - -This approach is particularly suitable for large applications or command-line tools with strong extensibility, allowing for the addition of new commands without modifying the entry code. - -### Asynchronous Command Handling - -For commands that need to execute asynchronously, you can use the `RunAsync` method: - -```csharp -await commandLine.AddHandler(async options => -{ - await ImportDataAsync(options); - return 0; -}) -.RunAsync(); -``` +1. `[Command]` supports multiple words, representing subcommands (e.g., `[Command("remote add")]`). +2. Absence of `[Command]`, or one with null/empty string, means default command (`[Command("")]`). +3. If multiple handlers match the same command, `CommandNameAmbiguityException` is thrown. +4. If any handler is asynchronous, you must use `RunAsync` instead of `Run` (otherwise compilation fails). ## URL Protocol Support -DotNetCampus.CommandLine supports parsing URL protocol strings: +DotNetCampus.CommandLine can parse a URL protocol string: +```ini +// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 ``` -dotnet-campus://open/document.txt?readOnly=true&mode=Display&silence=true&startup-sessions=89EA9D26-6464-4E71-BD04-AA6516063D83 -``` - -Features and usage of URL protocol parsing: - -1. The URL path part (such as `open/document.txt` in the example) will be parsed as positional arguments or command plus positional arguments - - The first part of the path can serve as a command (needs to be marked with the `[Command]` attribute) - - The subsequent path parts will be parsed as positional arguments -2. Query parameters (the part after `?`) will be parsed as named options -3. Collection type options can be passed multiple values by repeating parameter names, such as: `tags=csharp&tags=dotnet` -4. Special characters and non-ASCII characters in the URL will be automatically URL-decoded -## Naming Conventions and Best Practices +The example near the top expressed as URL: -To ensure better compatibility and user experience, we recommend using the kebab-case style for naming long options: - -```csharp -// Recommended -[Option('o', "output-file")] -public string OutputFile { get; init; } - -// Not recommended -[Option('o', "OutputFile")] -public string OutputFile { get; init; } +```ini +# `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` +dotnet-campus://1.txt/2.txt?count=20&test-name=BenchmarkTest&detail-level=High&debug ``` -Benefits of using kebab-case naming: - -1. Provides clearer word separation information (e.g., can guess "DotNet-Campus" rather than "Dot-Net-Campus") -2. Resolves digital subordination issues (e.g., whether "Version2Info" is "Version2-Info" or "Version-2-Info") -3. Better compatibility with various command-line styles - -## Source Generators, Interceptors, and Performance Optimization +Details: -DotNetCampus.CommandLine uses source code generator technology to significantly improve the performance of command-line parsing. The interceptors ([Interceptor](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md)) make the performance improvement even more impressive. +1. Collection options can repeat query names: `tags=csharp&tags=dotnet` +2. Special and non-ASCII characters are URL-decoded automatically -### How Interceptors Work +## Source Generators, Interceptors & Performance -When you call methods like `CommandLine.As()` or `CommandLine.AddHandler()`, the source generator automatically generates intercepting code that redirects the call to a high-performance code path generated at compile time. This significantly improves the performance of command-line argument parsing and object creation. - -For example, when you write the following code: - -```csharp -var options = CommandLine.Parse(args).As(); -``` +DotNetCampus.CommandLine leverages source generators and interceptors for major performance gains. -The source generator will intercept this call and automatically generate code similar to the following to replace the default way of implementing it by looking up creators in a dictionary (older versions used reflection): +### Example user code ```csharp -/// -/// Interceptor for method. Intercepts to improve performance. -/// -[global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xInterceptor @Program.cs */ "G4GJAK7udHFnPkRUqV6VzzgRAABQcm9ncmFtLmNz")] -public static T CommandLine_As_DotNetCampusCliTestsFakesOptions(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : global::DotNetCampus.Cli.Tests.Fakes.Options +public class BenchmarkOptions41 { - return (T)global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance(commandLine); -} -``` + [Option("debug")] + public required bool IsDebugMode { get; init; } -### Examples of Source Generator Generated Code + [Option('c', "count")] + public required int TestCount { get; init; } -Below is a simple command-line option type and its corresponding generated source code: + [Option('n', "test-name")] + public string? TestName { get; set; } -```csharp -// Type in user code -internal record DotNet03_MixedOptions -{ - [Option] - public int Number { get; init; } + [Option("test-category")] + public string? TestCategory { get; set; } - [Option] - public required string Text { get; init; } + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; - [Option] - public bool Flag { get; init; } + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; } ``` Corresponding generated source: ```csharp -#nullable enable -namespace DotNetCampus.Cli.Tests; - -/// -/// Helper for generating command-line options, commands, or handler functions for . -/// -internal sealed class DotNet03_MixedOptionsBuilder -{ - public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) - { - var caseSensitive = commandLine.DefaultCaseSensitive; - var result = new global::DotNetCampus.Cli.Tests.DotNet03_MixedOptions - { - Number = commandLine.GetOption("number") ?? default, - Text = commandLine.GetOption("text") ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option '--text'. Command line: {commandLine}", "Text"), - Flag = commandLine.GetOption("flag") ?? default, - // There is no positional argument to be initialized. - }; - // There is no option to be assigned. - // There is no positional argument to be assigned. - return result; - } -} -``` - -Method call in code: - -```csharp -_ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); -``` - -Corresponding generated source (interceptor): - -```csharp -#nullable enable - -namespace DotNetCampus.Cli.Compiler -{ - file static class Interceptors - { - /// - /// Interceptor for method. Intercepts to improve performance. - /// - [global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xInterceptor @Program.cs */ "G4GJAK7udHFnPkRUqV6VzzgRAABQcm9ncmFtLmNz")] - [global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xModule @Program.cs */ "G4GJAK7udHFnPkRUqV6VzxkSAABQcm9ncmFtLmNz")] - public static T CommandLine_As_DotNetCampusCliTestsFakesOptions(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : global::DotNetCampus.Cli.Tests.Fakes.Options - { - return (T)global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance(commandLine); - } - } -} - -namespace System.Runtime.CompilerServices -{ - [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : global::System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - _ = version; - _ = data; - } - } -} +// After AI translation is finished, human contributors will supplement it. ``` -Assembly command handler collection in code: +## Performance Data -```csharp -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; -``` +Source generator implementation yields very high parsing performance. -Corresponding generated source: +Parsing empty command line arguments: -```csharp -#nullable enable -namespace DotNetCampus.Cli.Tests.Fakes; +| Method | Mean | Error | StdDev | Gen0 | Allocated | +| ----------------------------- | -----------: | ---------: | ---------: | -----: | --------: | +| 'parse [] -v=4.1 -p=flexible' | 27.25 ns | 0.485 ns | 0.454 ns | 0.0143 | 240 B | +| 'parse [] -v=4.1 -p=dotnet' | 27.35 ns | 0.471 ns | 0.440 ns | 0.0143 | 240 B | +| 'parse [] -v=4.0 -p=flexible' | 97.16 ns | 0.708 ns | 0.628 ns | 0.0134 | 224 B | +| 'parse [] -v=4.0 -p=dotnet' | 95.90 ns | 0.889 ns | 0.742 ns | 0.0134 | 224 B | +| 'parse [] -v=3.x -p=parser' | 49.73 ns | 0.931 ns | 0.870 ns | 0.0239 | 400 B | +| 'parse [] -v=3.x -p=runtime' | 19,304.17 ns | 194.337 ns | 162.280 ns | 0.4272 | 7265 B | -/// -/// Provides a way to automatically collect and execute all command line handlers in this assembly. -/// -partial class AssemblyCommandHandler : global::DotNetCampus.Cli.Compiler.ICommandHandlerCollection -{ - public global::DotNetCampus.Cli.ICommandHandler? TryMatch(string? command, global::DotNetCampus.Cli.CommandLine cl) => command switch - { - null => throw new global::DotNetCampus.Cli.Exceptions.CommandVerbAmbiguityException($"Multiple command handlers match the same command name 'null': AmbiguousOptions, CollectionOptions, ComparedOptions, DefaultCommandHandler, DictionaryOptions, FakeCommandOptions, Options, PrimaryOptions, UnlimitedValueOptions, ValueOptions.", null), - // Type EditOptions does not implement the ICommandHandler interface, so it cannot be dispatched uniformly and must be called by the developer separately. - "Fake" => (global::DotNetCampus.Cli.ICommandHandler)global::DotNetCampus.Cli.Tests.Fakes.FakeCommandHandlerBuilder.CreateInstance(cl), - // Type PrintOptions does not implement the ICommandHandler interface, so it cannot be dispatched uniformly and must be called by the developer separately. - // Type ShareOptions does not implement the ICommandHandler interface, so it cannot be dispatched uniformly and must be called by the developer separately. - _ => null, - }; -} -``` +Parsing GNU style command line arguments: -## Performance Data +```bash +test DotNetCampus.CommandLine.Performance.dll DotNetCampus.CommandLine.Sample.dll DotNetCampus.CommandLine.Test.dll -c 20 --test-name BenchmarkTest --detail-level High --debug +``` + +| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated | +| -------------------------------- | ------------- | ------------- | ----------: | --------: | --------: | -----: | --------: | +| 'parse [GNU] -v=4.1 -p=flexible' | .NET 10.0 | .NET 10.0 | 355.9 ns | 4.89 ns | 4.58 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.1 -p=gnu' | .NET 10.0 | .NET 10.0 | 339.7 ns | 6.81 ns | 7.57 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.0 -p=flexible' | .NET 10.0 | .NET 10.0 | 945.9 ns | 14.87 ns | 13.19 ns | 0.1583 | 2656 B | +| 'parse [GNU] -v=4.0 -p=gnu' | .NET 10.0 | .NET 10.0 | 882.1 ns | 11.30 ns | 10.57 ns | 0.1631 | 2736 B | +| 'parse [GNU] -v=3.x -p=parser' | .NET 10.0 | .NET 10.0 | 495.7 ns | 9.26 ns | 9.09 ns | 0.1040 | 1752 B | +| 'parse [GNU] -v=3.x -p=runtime' | .NET 10.0 | .NET 10.0 | 18,025.5 ns | 194.73 ns | 162.61 ns | 0.4883 | 8730 B | +| 'NuGet: ConsoleAppFramework' | .NET 10.0 | .NET 10.0 | 134.1 ns | 2.70 ns | 2.65 ns | 0.0215 | 360 B | +| 'parse [GNU] -v=4.1 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 624.3 ns | 7.06 ns | 6.60 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.1 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 600.3 ns | 6.72 ns | 6.28 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.0 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 1,395.6 ns | 20.43 ns | 19.11 ns | 0.1507 | 2529 B | +| 'parse [GNU] -v=4.0 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 1,438.1 ns | 19.84 ns | 18.55 ns | 0.1545 | 2609 B | +| 'parse [GNU] -v=3.x -p=parser' | NativeAOT 9.0 | NativeAOT 9.0 | 720.8 ns | 7.47 ns | 6.99 ns | 0.1030 | 1737 B | +| 'parse [GNU] -v=3.x -p=runtime' | NativeAOT 9.0 | NativeAOT 9.0 | NA | NA | NA | NA | NA | +| 'NuGet: ConsoleAppFramework' | NativeAOT 9.0 | NativeAOT 9.0 | 195.3 ns | 3.76 ns | 3.69 ns | 0.0234 | 392 B | + +Notes: + +1. `parse` means calling `CommandLine.Parse` +2. `handle` means calling `CommandLine.AddHandler` +3. Brackets `[Xxx]` show the style of passed arguments +4. `--flexible`, `--gnu` etc. indicate parser style used (matching improves efficiency) +5. `-v=3.x -p=parser` shows old manually-written parsers (best performance but limited syntax support) +6. `-v=3.x -p=runtime` shows old reflection-based runtime parser +7. `-v=4.0` vs `-v=4.1` illustrate performance evolution +8. `NuGet: ...` rows show performance of other libraries +9. `parse [URL]` rows (omitted above) indicate URL protocol parsing performance + +Author's perspective (@walterlv): + +1. Fastest library observed is [ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework); ours is close and same order of magnitude. +2. Great thanks to ConsoleAppFramework's pursuit of zero dependencies / allocations / reflection; it motivated the current version (`-v4.1`). +3. ConsoleAppFramework targets extreme performance (sacrificing some syntax breadth). Our goal: full-featured plus high performance—so we sit in the same tier but can't surpass it. Choose based on audience and requirements. -The source code generator implementation provides extremely high command line parsing performance: - -| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | -|---------------------------------------- |----------------:|--------------:|--------------:|----------------:|-------:|-------:|----------:| -| 'parse [] --flexible' | 39.16 ns | 0.402 ns | 0.357 ns | 39.15 ns | 0.0124 | - | 208 B | -| 'parse [] --gnu' | 38.22 ns | 0.518 ns | 0.459 ns | 38.30 ns | 0.0124 | - | 208 B | -| 'parse [] --posix' | 38.45 ns | 0.792 ns | 0.741 ns | 38.45 ns | 0.0124 | - | 208 B | -| 'parse [] --dotnet' | 42.14 ns | 0.878 ns | 2.588 ns | 42.06 ns | 0.0124 | - | 208 B | -| 'parse [] --powershell' | 38.67 ns | 0.772 ns | 1.451 ns | 38.42 ns | 0.0124 | - | 208 B | -| 'parse [] -v=3.x -p=parser' | 44.07 ns | 0.665 ns | 0.841 ns | 44.08 ns | 0.0220 | - | 368 B | -| 'parse [] -v=3.x -p=runtime' | 365.36 ns | 7.186 ns | 13.319 ns | 361.47 ns | 0.0367 | - | 616 B | -| 'parse [PS1] --flexible' | 907.15 ns | 17.887 ns | 38.504 ns | 899.46 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] --dotnet' | 969.51 ns | 18.977 ns | 31.179 ns | 964.56 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] -v=3.x -p=parser' | 448.38 ns | 8.883 ns | 13.830 ns | 445.91 ns | 0.0715 | - | 1200 B | -| 'parse [PS1] -v=3.x -p=runtime' | 835.83 ns | 16.055 ns | 38.774 ns | 830.59 ns | 0.0858 | - | 1448 B | -| 'parse [CMD] --flexible' | 932.31 ns | 18.636 ns | 40.907 ns | 936.14 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] --dotnet' | 877.96 ns | 8.846 ns | 9.832 ns | 877.67 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] -v=3.x -p=parser' | 438.09 ns | 8.591 ns | 11.469 ns | 433.77 ns | 0.0715 | - | 1200 B | -| 'parse [CMD] -v=3.x -p=runtime' | 822.05 ns | 16.417 ns | 25.560 ns | 811.08 ns | 0.0858 | - | 1448 B | -| 'parse [GNU] --flexible' | 880.14 ns | 17.627 ns | 36.794 ns | 878.35 ns | 0.1574 | - | 2648 B | -| 'parse [GNU] --gnu' | 811.59 ns | 13.691 ns | 20.492 ns | 805.61 ns | 0.1554 | - | 2608 B | -| 'parse [GNU] -v=3.x -p=parser' | 492.48 ns | 9.757 ns | 11.615 ns | 491.95 ns | 0.0896 | - | 1512 B | -| 'parse [GNU] -v=3.x -p=runtime' | 873.40 ns | 15.873 ns | 24.713 ns | 865.86 ns | 0.1049 | - | 1760 B | -| 'handle [Edit,Print] --flexible' | 693.30 ns | 13.894 ns | 28.066 ns | 681.77 ns | 0.2375 | 0.0019 | 3984 B | -| 'handle [Edit,Print] -v=3.x -p=parser' | 949.15 ns | 18.959 ns | 25.952 ns | 939.97 ns | 0.2775 | 0.0038 | 4648 B | -| 'handle [Edit,Print] -v=3.x -p=runtime' | 6,232.90 ns | 122.601 ns | 217.924 ns | 6,190.80 ns | 0.2594 | - | 4592 B | -| 'parse [URL]' | 2,942.05 ns | 54.322 ns | 76.152 ns | 2,926.04 ns | 0.4578 | - | 7704 B | -| 'parse [URL] -v=3.x -p=parser' | 121.43 ns | 2.457 ns | 5.496 ns | 121.10 ns | 0.0440 | - | 736 B | -| 'parse [URL] -v=3.x -p=runtime' | 462.92 ns | 9.017 ns | 10.023 ns | 464.26 ns | 0.0587 | - | 984 B | -| 'NuGet: CommandLineParser' | 212,745.53 ns | 4,237.822 ns | 11,384.635 ns | 211,418.82 ns | 5.3711 | - | 90696 B | -| 'NuGet: System.CommandLine' | 1,751,023.59 ns | 34,134.634 ns | 50,034.108 ns | 1,727,339.45 ns | 3.9063 | - | 84138 B | - -Where: -1. `parse` indicates calling the `CommandLine.Parse` method -2. `handle` indicates calling the `CommandLine.AddHandler` method -3. Square brackets `[Xxx]` indicate the style of command-line arguments passed in -4. `--flexible`, `--gnu`, etc. indicate the parser style used when parsing the incoming command line (highest efficiency when matched) -5. `-v=3.x -p=parser` indicates the performance of manually written parsers passed in the old version (best performance, but the old version supports fewer command-line specifications, and many legal command formats are not supported) -6. `-v=3.x -p=runtime` indicates the performance of the old version using the default reflection parser -7. `NuGet: CommandLineParser` and `NuGet: System.CommandLine` indicate the performance when using the corresponding NuGet packages to parse command-line arguments -8. `parse [URL]` indicates the performance when parsing URL protocol strings - -Thanks to source generators and interceptors, the new version: -1. Completes a parsing in about 0.8μs (microseconds) (Benchmark) -2. During application startup, completing one parsing only takes about 34μs -3. During application startup, including dll loading and type initialization, one parsing takes about 8ms (using AOT compilation can reduce it back to 34μs). diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index 31e64b9a..45b3cf36 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -340,278 +340,7 @@ public class BenchmarkOptions41 对应生成的源: ```csharp -#nullable enable -using global::System; -using global::DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Performance.Fakes; - -/// -/// 辅助 生成命令行选项、子命令或处理函数的创建。 -/// -public sealed class BenchmarkOptions41Builder(global::DotNetCampus.Cli.CommandLine commandLine) -{ - public static global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) - { - return new DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41Builder(commandLine).Build(); - } - - private global::DotNetCampus.Cli.Compiler.BooleanArgument IsDebugMode = new(); - - private global::DotNetCampus.Cli.Compiler.NumberArgument TestCount = new(); - - private global::DotNetCampus.Cli.Compiler.StringArgument TestName = new(); - - private global::DotNetCampus.Cli.Compiler.StringArgument TestCategory = new(); - - private __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ DetailLevel = new(); - - private global::DotNetCampus.Cli.Compiler.StringListArgument TestItems = new(); - - public global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 Build() - { - if (commandLine.RawArguments.Count is 0) - { - return BuildDefault(); - } - - var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(commandLine, "BenchmarkOptions41", 0) - { - MatchLongOption = MatchLongOption, - MatchShortOption = MatchShortOption, - MatchPositionalArguments = MatchPositionalArguments, - AssignPropertyValue = AssignPropertyValue, - }; - parser.Parse().ThrowIfError(); - return BuildCore(commandLine); - } - - private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, global::DotNetCampus.Cli.CommandNamingPolicy namingPolicy) - { - // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。 - switch (longOption) - { - case "debug": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); - case "count": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); - case "test-name": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); - case "test-category": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); - case "detail-level": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); - } - - var defaultComparison = defaultCaseSensitive - ? global::System.StringComparison.Ordinal - : global::System.StringComparison.OrdinalIgnoreCase; - - // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。 - if (namingPolicy.SupportsOrdinal()) - { - if (longOption.Equals("debug".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); - } - if (longOption.Equals("count".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("test-name".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("test-category".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("detail-level".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); - } - } - - // 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。 - if (namingPolicy.SupportsPascalCase()) - { - if (longOption.Equals("Debug".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean); - } - if (longOption.Equals("Count".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("TestName".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("TestCategory".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (longOption.Equals("DetailLevel".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); - } - } - - return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; - } - - private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive) - { - // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。 - switch (shortOption) - { - // 属性 IsDebugMode 没有短名称,无需匹配。 - case "c": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); - case "n": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); - // 属性 TestCategory 没有短名称,无需匹配。 - case "d": - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); - } - - var defaultComparison = defaultCaseSensitive - ? global::System.StringComparison.Ordinal - : global::System.StringComparison.OrdinalIgnoreCase; - - // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。 - // 属性 IsDebugMode 没有短名称,无需匹配。 - if (shortOption.Equals("c".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal); - } - if (shortOption.Equals("n".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal); - } - // 属性 TestCategory 没有短名称,无需匹配。 - if (shortOption.Equals("d".AsSpan(), defaultComparison)) - { - return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal); - } - - return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch; - } - - private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex) - { - // 属性 TestItems 覆盖了所有位置参数,直接匹配。 - return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("TestItems", 5, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal); - } - - private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) - { - switch (propertyIndex) - { - case 0: - IsDebugMode = IsDebugMode.Assign(value); - break; - case 1: - TestCount = TestCount.Assign(value); - break; - case 2: - TestName = TestName.Assign(value); - break; - case 3: - TestCategory = TestCategory.Assign(value); - break; - case 4: - DetailLevel = DetailLevel.Assign(value); - break; - case 5: - TestItems = TestItems.Append(value); - break; - } - } - - private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 BuildCore(global::DotNetCampus.Cli.CommandLine commandLine) - { - var result = new global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 - { - // 1. There is no [RawArguments] property to be initialized. - - // 2. [Option] - IsDebugMode = IsDebugMode.ToBoolean() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'debug'. Command line: {commandLine}", "IsDebugMode"), - TestCount = TestCount.ToInt32() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'count'. Command line: {commandLine}", "TestCount"), - - // 3. [Value] - TestItems = TestItems.ToList(), - }; - - // 1. There is no [RawArguments] property to be assigned. - - // 2. [Option] - if (TestName.ToString() is { } o0) - { - result.TestName = o0; - } - if (TestCategory.ToString() is { } o1) - { - result.TestCategory = o1; - } - if (DetailLevel.ToDetailLevel() is { } o2) - { - result.DetailLevel = o2; - } - - // 3. There is no [Value] property to be assigned. - - return result; - } - - private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions41 BuildDefault() - { - throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!); - } - - /// - /// Provides parsing and assignment for the enum type . - /// - private readonly record struct __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ - { - /// - /// Indicates whether to ignore exceptions when parsing fails. - /// - public bool IgnoreExceptions { get; init; } - - /// - /// Stores the parsed enum value. - /// - private global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? Value { get; init; } - - /// - /// Assigns a value when a command line input is parsed. - /// - /// The parsed string value. - public __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ Assign(ReadOnlySpan value) - { - Span lowerValue = stackalloc char[value.Length]; - for (var i = 0; i < value.Length; i++) - { - lowerValue[i] = char.ToLowerInvariant(value[i]); - } - global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? newValue = lowerValue switch - { - "low" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Low, - "medium" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Medium, - "high" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.High, - _ when IgnoreExceptions => null, - _ => throw new global::DotNetCampus.Cli.Exceptions.CommandLineParseValueException($"Cannot convert '{value.ToString()}' to enum type 'DotNetCampus.Cli.Performance.Fakes.DetailLevel'."), - }; - return this with { Value = newValue }; - } - - /// - /// Converts the parsed value to the enum type. - /// - public global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? ToDetailLevel() => Value; - } -} +// 在 AI 翻译完成后,人类将补充它。 ``` ## 性能数据 diff --git a/docs/zh-hant/README.md b/docs/zh-hant/README.md index 0b8e95a3..4ef8051f 100644 --- a/docs/zh-hant/README.md +++ b/docs/zh-hant/README.md @@ -1,5 +1,4 @@ - -# 命令行解析 +# 命令列解析 | [English][en] | [简体中文][zh-hans] | [繁體中文][zh-hant] | | ------------- | ------------------- | ------------------- | @@ -8,7 +7,7 @@ [zh-hans]: /docs/zh-hans/README.md [zh-hant]: /docs/zh-hant/README.md -DotNetCampus.CommandLine 提供了簡單而高性能的命令行解析功能,得益於源代碼生成器的加持,它現在提供了更高效的解析能力和更友好的開發體驗。所有功能都位於 DotNetCampus.Cli 命名空間下。 +DotNetCampus.CommandLine 提供簡單且高效能的命令列解析功能。得益於原始碼產生器(以及攔截器),它現在提供更高效率的解析能力與更友善的開發體驗。所有功能均位於 `DotNetCampus.Cli` 命名空間下。 ## 快速使用 @@ -17,259 +16,210 @@ class Program { static void Main(string[] args) { - // 從命令行參數創建一個 CommandLine 類型的新實例 + // 從命令列參數建立一個新的 CommandLine 執行個體 var commandLine = CommandLine.Parse(args); - // 將命令行解析為 Options 類型的實例 - // 源生成器會自動為你處理解析過程,無需手動創建解析器 + // 將命令列解析為 Options 型別的執行個體 + // 原始碼產生器會自動處理解析過程,無需手動建立解析器 var options = commandLine.As(); - // 接下來,使用你的 options 對象編寫其他的功能 + // 接下來,使用 options 物件撰寫其他功能 } } ``` -你需要定義一個包含命令行參數映射的類型: +你需要定義一個包含命令列參數對應的型別: ```csharp -class Options +public class Options { - [Value(0)] - public required string FilePath { get; init; } - - [Option('s', "silence")] - public bool IsSilence { get; init; } + [Option("debug")] + public required bool IsDebugMode { get; init; } - [Option('m', "mode")] - public string? StartMode { get; init; } - - [Option("startup-sessions")] - public IReadOnlyList StartupSessions { get; init; } = []; -} -``` + [Option('c', "count")] + public required int TestCount { get; init; } -然後在命令行中使用不同風格的命令填充這個類型的實例。庫支持多種命令行風格: + [Option('n', "test-name")] + public string? TestName { get; set; } -### Windows PowerShell 風格 + [Option("test-category")] + public string? TestCategory { get; set; } -```powershell -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s -Mode Edit -StartupSessions A B C -``` + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; -### Windows CMD 風格 + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; +} -```cmd -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" /s /Mode Edit /StartupSessions A B C +public enum DetailLevel +{ + Low, + Medium, + High, +} ``` -### Linux/GNU 風格 - -```bash -$ demo.exe "C:/Users/lvyi/Desktop/demo.txt" -s --mode Edit --startup-sessions A --startup-sessions B --startup-sessions C -``` +然後在命令列中使用不同風格的命令填充這個型別的執行個體。程式庫支援多種命令列風格: -### .NET CLI 風格 -``` -> demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s:true --mode:Edit --startup-sessions:A;B;C -``` +| 風格 | 範例 | +| --------------- | ------------------------------------------------------------------------------------------ | +| DotNet | `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` | +| PowerShell | `demo.exe 1.txt 2.txt 3.txt -c 20 -TestName BenchmarkTest -DetailLevel High -Debug` | +| CMD | `demo.exe 1.txt 2.txt 3.txt /c 20 /TestName BenchmarkTest /DetailLevel High /Debug` | +| Gnu | `demo.exe 1.txt 2.txt 3.txt -c 20 --test-name BenchmarkTest --detail-level High --debug` | +| 彈性 (Flexible) | `demo.exe 1.txt 2.txt 3.txt --count:20 /TestName BenchmarkTest --detail-level=High -Debug` | -## 命令行風格 +## 命令列風格 -DotNetCampus.CommandLine 支持多種命令行風格,你可以在解析時指定使用哪種風格: +DotNetCampus.CommandLine 支援多種命令列風格,你可以在解析時指定使用哪種風格: ```csharp -// 使用 .NET CLI 風格解析命令行參數 +// 使用 .NET CLI 風格解析命令列參數 var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); ``` -支持的風格包括: - -- `CommandLineStyle.Flexible`(默認):智能識別多種風格,默認大小寫不敏感,是 DotNet/GNU/PowerShell 風格的有效組合 - - 支持前面示例中所有風格的命令行參數,可正確解析 - - 完整支持 DotNet 風格的所有命令行功能(包括列表和字典) - - 支持 GNU 風格中除短名稱接參數(如 `-o1.txt`)和短名稱縮寫(如 `-abc` 表示 `-a -b -c`)外的所有功能 - - 由於 Posix 規則限制嚴格,Flexible 風格自然兼容 Posix 風格 - - DotNet 風格本身兼容 PowerShell 命令行風格,因此 Flexible 風格也支持 PowerShell 風格 -- `CommandLineStyle.Gnu`:符合 GNU 規範的風格,默認大小寫敏感 -- `CommandLineStyle.Posix`:符合 POSIX 規範的風格,默認大小寫敏感 -- `CommandLineStyle.DotNet`:.NET CLI 風格,默認大小寫不敏感 -- `CommandLineStyle.PowerShell`:PowerShell 風格,默認大小寫不敏感 - -## 數據類型支持 - -庫支持多種數據類型的解析: - -1. **基本類型**: 字符串、整數、布爾值、枚舉等 -2. **集合類型**: 數組、列表、只讀集合、不可變集合 -3. **字典類型**: IDictionary、IReadOnlyDictionary、ImmutableDictionary等 - -### 布爾類型選項 - -對於布爾類型的選項,在命令行中有多種指定方式: - -- 僅指定選項名稱,表示 `true`:`-s` 或 `--silence` -- 顯式指定值:`-s:true`、`-s=false`、`--silence:on`、`--silence=off` - -### 集合類型選項 - -對於集合類型的選項,可以通過多次指定同一選項,或使用分號分隔多個值: - -``` -demo.exe --files file1.txt --files file2.txt -demo.exe --files:file1.txt;file2.txt;file3.txt -``` - -### 字典類型選項 - -對於字典類型的選項,支持多種傳入方式: - -``` -demo.exe --properties key1=value1 --properties key2=value2 -demo.exe --properties:key1=value1;key2=value2 -``` - -## 位置參數 - -除了命名選項外,你還可以使用位置參數,通過 `ValueAttribute` 指定參數的位置: +支援的風格包含: + +- `CommandLineStyle.Flexible`(預設):彈性風格,於各種風格間提供最大相容性,預設大小寫不敏感 +- `CommandLineStyle.DotNet`:.NET CLI 風格,預設大小寫敏感 +- `CommandLineStyle.Gnu`:符合 GNU 規範,預設大小寫敏感 +- `CommandLineStyle.Posix`:符合 POSIX 規範,預設大小寫敏感 +- `CommandLineStyle.PowerShell`:PowerShell 風格,預設大小寫不敏感 + +預設情況下,這些風格的詳細差異如下: + +| 風格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| -------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | +| 大小寫 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 長選項 | 支援 | 支援 | 支援 | 不支援 | 支援 | 支援 | +| 短選項 | 支援 | 支援 | 支援 | 支援 | 支援 | 不支援 | +| 選項值 `=` | -o=value | -o=value | -o=value | | | option=value | +| 選項值 `:` | -o:value | -o:value | | | | | +| 選項值 空白 | -o value | -o value | -o value | -o value | -o value | | +| 布林選項 | -o | -o | -o | -o | -o | option | +| 布林選項帶值 | -o=true | -o=true | | | -o:true | option=true | +| 布林值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布林值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布林值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布林值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 集合選項 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合選項 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | +| 集合選項 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | +| 集合選項 空白 | -o A B C | -o A B C | | | -o A B C | | +| 字典選項 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 多短布林合併 | 不支援 | 不支援 | -abc | -abc | 不支援 | 不支援 | +| 單短選項多字元 | -ab | -ab | 不支援 | 不支援 | -ab | 不支援 | +| 短選項直接帶值 | 不支援 | 不支援 | -o1.txt | 不支援 | 不支援 | 不支援 | +| 長選項前綴 | `--` `-` `/` | `--` | `--` | 不支援 | `-` `/` | | +| 短選項前綴 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | + +## 命名法 + +1. 在程式碼中定義選項時,應使用 kebab-case 命名法 + - [為什麼要這麼做?](https://github.com/dotnet-campus/DotNetCampus.CommandLine/blob/main/docs/analyzers/DCL101.md) + - 若推測你寫的不是 kebab-case,會提供警告 DCL101 + - 你可以忽略該警告;無論實際字串為何,都當作 kebab-case(提供無歧義的單詞邊界資訊,見下例) +2. 當你定義了被視為 kebab-case 的字串後 + - 依據設定的解析風格,可使用 kebab-case / PascalCase / camelCase 三種風格 + +範例: ```csharp -class FileOptions +[Command("open command-line")] +public class Options { - [Value(0)] - public string InputFile { get; init; } - - [Value(1)] - public string OutputFile { get; init; } - - [Option('v', "verbose")] - public bool Verbose { get; init; } + [Option('o', "option-name")] + public required string OptionName { get; init; } } ``` -使用方式: - -``` -demo.exe input.txt output.txt --verbose -``` - -你也可以捕獲多個位置參數到一個數組或集合中: - -```csharp -class MultiFileOptions -{ - [Value(0, Length = int.MaxValue)] - public string[] Files { get; init; } = []; -} -``` +此處有兩個 kebab-case:`Command` 特性與 `Option` 特性。可接受: -## 組合使用選項和位置參數 +- DotNet/Gnu:`demo.exe open command-line --option-name value` +- PowerShell:`demo.exe Open CommandLine -OptionName value` +- CMD:`demo.exe Open CommandLine /optionName value` -`ValueAttribute` 和 `OptionAttribute` 可以同時應用於同一個屬性: +若改寫為其他風格,可能出現與預期不同(或是刻意的)結果: ```csharp -class Options +#pragma warning disable DCL101 +[Command("Open CommandLine")] +public class Options { - [Value(0), Option('f', "file")] - public string FilePath { get; init; } + // 分析器警告:OptionName 不是 kebab-case,可視需要抑制 DCL101。 + [Option('o', "OptionName")] + public required string OptionName { get; init; } } +#pragma warning restore DCL101 ``` -這樣,以下命令行都會將文件路徑賦值給 `FilePath` 屬性: - -``` -demo.exe file.txt -demo.exe -f file.txt -demo.exe --file file.txt -``` +因為仍視為 kebab-case,於是可接受: -## 必需選項與可選選項 +- DotNet/Gnu:`demo.exe Open CommandLine --OptionName value` +- PowerShell:`demo.exe Open CommandLine -OptionName value` +- CMD:`demo.exe Open CommandLine /optionName value` -在C# 11及以上版本中,可以使用`required`修飾符標記必需的選項: +## 資料型別 -```csharp -class Options -{ - [Option('i', "input")] - public required string InputFile { get; init; } // 必需選項 - - [Option('o', "output")] - public string? OutputFile { get; init; } // 可選選項 -} -``` +程式庫支援多種資料型別: -如果未提供必需選項,解析時會拋出`RequiredPropertyNotAssignedException`異常。 +1. **基本型別**:字串、整數、布林、列舉等 +2. **集合型別**:陣列、List、唯讀集合、不可變集合 +3. **字典型別**:`IDictionary`、`IReadOnlyDictionary`、`ImmutableDictionary` 等 -## 屬性初始值與訪問器修飾符 +如何透過命令列傳入,詳見前面的大型表格。 -在定義選項類型時,需要注意屬性初始值與訪問器修飾符(`init`、`required`)之間的關係: +## 必需選項與預設值 -```csharp -class Options -{ - // 錯誤示例:當使用 init 或 required 時,默認值將被忽略 - [Option('f', "format")] - public string Format { get; init; } = "json"; // 默認值不會生效! - - // 正確示例:使用 set 以保留默認值 - [Option('f', "format")] - public string Format { get; set; } = "json"; // 默認值會正確保留 -} -``` +定義屬性時,可用下列標記: -### 關於屬性初始值的重要說明 +1. 使用 `required` 標記選項為必需 +2. 使用 `init` 標記選項為唯讀(初始化後不可改) +3. 使用 `?` 標記選項可為 null -1. **使用 `init` 或 `required` 時的行為**: - - 當屬性包含 `required` 或 `init` 修飾符時,屬性的初始值會被忽略 - - 如果命令行參數中未提供該選項的值,屬性將被設置為 `default(T)`(對於引用類型為 `null`) - - 這是由 C# 語言特性決定的,命令行庫如果希望突破此限制需要針對所有屬性排列組合進行處理,顯然是非常浪費的 +實際指派的值依下表行為: -2. **保留默認值的方式**: - - 如果需要為屬性提供默認值,應使用 `{ get; set; }` 而非 `{ get; init; }` +| required | init | 集合屬性 | nullable | 行為 | 說明 | +| -------- | ---- | -------- | -------- | ---------- | ------------------------------- | +| 1 | _ | _ | _ | 擲出例外 | 必須傳入,缺少則擲出例外 | +| 0 | 1 | 1 | _ | 空集合 | 集合永不為 null,缺少則給空集合 | +| 0 | 1 | 0 | 1 | null | 可為 null,缺少則給 null | +| 0 | 1 | 0 | 0 | 預設值 | 不可為 null,缺少則 default(T) | +| 0 | 0 | _ | _ | 保留初始值 | 非必需/非立即,保留定義時初始值 | -3. **可空類型與警告處理**: - - 對於非必需的引用類型屬性,應將其標記為可空(如 `string?`)以避免可空警告 - - 對於值類型(如 `int`、`bool`),如果想保留默認值而非 `null`,不應將其標記為可空 +- 1 = 標記過 +- 0 = 未標記 +- _ = 不論 -示例: +1. 可空行為對參考與值型別一致(差別只是 default 對參考型別為 null) +2. 缺少必需選項會擲出 `RequiredPropertyNotAssignedException` +3. 「保留初始值」表示可直接在屬性定義時給初值: ```csharp -class OptionsBestPractice -{ - // 必需選項:使用 required,無需擔心默認值 - [Option("input")] - public required string InputFile { get; init; } - - // 可選選項:標記為可空類型以避免警告 - [Option("output")] - public string? OutputFile { get; init; } - - // 需要默認值的選項:使用 set 而非 init - [Option("format")] - public string Format { get; set; } = "json"; - - // 值類型選項:不需要標記為可空 - [Option("count")] - public int Count { get; set; } = 1; -} +// 注意:只有未使用 required 與 init 時,初值才會生效。 +[Option('o', "option-name")] +public string OptionName { get; set; } = "Default Value"; ``` -## 命令處理與謂詞 - -你可以使用命令處理器模式處理不同的命令(謂詞),類似於`git commit`、`git push`等。DotNetCampus.CommandLine 提供了多種添加命令處理器的方式: +## 命令與子命令 -### 1. 使用委託處理命令 +可使用命令處理器模式處理不同命令,類似 `git commit`、`git remote add`。提供多種方式: -最簡單的方式是通過委託處理命令,將命令選項類型和處理邏輯分離: +### 1. 使用委派處理 ```csharp var commandLine = CommandLine.Parse(args); -commandLine.AddHandler(options => { /* 處理add命令 */ }) - .AddHandler(options => { /* 處理remove命令 */ }) +commandLine.AddHandler(options => { /* 處理 add */ }) + .AddHandler(options => { /* 處理 remove */ }) .Run(); ``` -定義命令選項類時使用`Command`特性標記命令: - ```csharp [Command("add")] public class AddOptions @@ -286,9 +236,7 @@ public class RemoveOptions } ``` -### 2. 使用 ICommandHandler 接口 - -對於更複雜的命令處理邏輯,你可以創建實現 `ICommandHandler` 接口的類,將命令選項和處理邏輯封裝在一起: +### 2. `ICommandHandler` 介面 ```csharp [Command("convert")] @@ -296,259 +244,144 @@ internal class ConvertCommandHandler : ICommandHandler { [Option('i', "input")] public required string InputFile { get; init; } - + [Option('o', "output")] public string? OutputFile { get; init; } - + [Option('f', "format")] public string Format { get; set; } = "json"; - + public Task RunAsync() { - // 實現命令處理邏輯 + // 命令處理邏輯 Console.WriteLine($"Converting {InputFile} to {Format} format"); // ... - return Task.FromResult(0); // 返回退出碼 + return Task.FromResult(0); // 結束代碼 } } ``` -然後直接添加到命令行解析器中: - ```csharp var commandLine = CommandLine.Parse(args); -commandLine.AddHandler() +commandLine + .AddHandler() + .AddHandler() + .AddHandler(options => { /* 處理 remove */ }) .Run(); ``` -### 3. 使用程序集自動發現命令處理器 - -為了更方便地管理大量命令且無需手動逐個添加,可以使用程序集自動發現功能,自動添加程序集中所有實現了 `ICommandHandler` 接口的類: - -```csharp -// 定義一個部分類用於標記自動發現命令處理器 -[CollectCommandHandlersFromThisAssembly] -internal partial class AssemblyCommandHandler; - -// 在程序入口添加所有命令處理器 -var commandLine = CommandLine.Parse(args); -commandLine.AddHandlers() - .Run(); -``` +### 說明 -通常,處理器類需要添加 `[Command]` 特性並實現 `ICommandHandler` 接口,它就會被自動發現和添加: +1. `[Command]` 支援多個單字,表示子命令(例:`[Command("remote add")]`)。 +2. 未標記 `[Command]`,或標記為 null / 空字串,表示預設命令(`[Command("")]`)。 +3. 多個處理器匹配同一命令會擲出 `CommandNameAmbiguityException`。 +4. 若有任何處理器為非同步,必須使用 `RunAsync`(否則編譯失敗)。 -```csharp -[Command("sample")] -internal class SampleCommandHandler : ICommandHandler -{ - [Option("SampleProperty")] - public required string Option { get; init; } +## URL 協議支援 - [Value(Length = int.MaxValue)] - public string? Argument { get; init; } +可解析 URL 協議字串: - public Task RunAsync() - { - // 實現命令處理邏輯 - return Task.FromResult(0); - } -} +```ini +// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 ``` -此外,你也可以創建一個沒有 `[Command]` 特性的命令處理器作為默認處理器。在程序集中最多只能有一個沒有 `[Command]` 特性的命令處理器,它將在沒有其他命令匹配時被使用: - -```csharp -// 沒有 [Command] 特性的默認處理器 -internal class DefaultCommandHandler : ICommandHandler -{ - [Option('h', "help")] - public bool ShowHelp { get; init; } +開頭示例命令列可寫成: - public Task RunAsync() - { - // 處理默認命令,如顯示幫助信息等 - if (ShowHelp) - { - Console.WriteLine("顯示幫助信息..."); - } - return Task.FromResult(0); - } -} +```ini +# `demo.exe 1.txt 2.txt -c:20 --test-name:BenchmarkTest --detail-level=High --debug` +dotnet-campus://1.txt/2.txt?count=20&test-name=BenchmarkTest&detail-level=High&debug ``` -這種方式特別適合大型應用或擴展性強的命令行工具,可以在不修改入口代碼的情況下添加新命令。 +特別說明: -### 異步命令處理 +1. 集合型別可重複參數名:`tags=csharp&tags=dotnet` +2. URL 中特殊與非 ASCII 字元會自動進行解碼 -對於需要異步執行的命令處理,可以使用`RunAsync`方法: - -```csharp -await commandLine.AddHandler(async options => -{ - await ImportDataAsync(options); - return 0; -}) -.RunAsync(); -``` +## 原始碼產生器、攔截器與效能 -## URL協議支持 +使用原始碼產生器與攔截器大幅提升效能。 -DotNetCampus.CommandLine 支持解析 URL 協議字符串: - -``` -dotnet-campus://open/document.txt?readOnly=true&mode=Display&silence=true&startup-sessions=89EA9D26-6464-4E71-BD04-AA6516063D83 -``` - -URL協議解析的特點和用法: - -1. URL路徑部分(如示例中的 `open/document.txt`)會被解析為位置參數或謂詞加位置參數 - - 路徑的第一部分可作為謂詞(需標記 `[Command]` 特性) - - 隨後的路徑部分會被解析為位置參數 -2. 查詢參數(`?` 後的部分)會被解析為命名選項 -3. 集合類型選項可通過重複參數名傳入多個值,如:`tags=csharp&tags=dotnet` -4. URL中的特殊字符和非ASCII字符會自動進行URL解碼 - -## 命名約定與最佳實踐 - -為確保更好的兼容性和用戶體驗,我們建議使用 kebab-case 風格命名長選項: +### 使用者程式碼範例 ```csharp -// 推薦 -[Option('o', "output-file")] -public string OutputFile { get; init; } - -// 不推薦 -[Option('o', "OutputFile")] -public string OutputFile { get; init; } -``` - -使用kebab-case命名的好處: - -1. 提供更清晰的單詞分割信息(如能猜出"DotNet-Campus"而不是"Dot-Net-Campus") -2. 解決數字從屬問題(如"Version2Info"是"Version2-Info"還是"Version-2-Info") -3. 與多種命令行風格更好地兼容 - -## 源生成器、攔截器與性能優化 - -DotNetCampus.CommandLine 使用源代碼生成器技術大幅提升了命令行解析的性能。其中的攔截器([Interceptor](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md))讓性能提升發揮得更淋漓盡致。 - -### 攔截器的工作原理 +public class BenchmarkOptions41 +{ + [Option("debug")] + public required bool IsDebugMode { get; init; } -當你調用 `CommandLine.As()` 或 `CommandLine.AddHandler()` 等方法時,源生成器會自動生成攔截代碼,將調用重定向到編譯時生成的高性能代碼路徑。這使得命令行參數解析和對象創建的性能得到了大幅提升。 + [Option('c', "count")] + public required int TestCount { get; init; } -例如,當你編寫以下代碼時: + [Option('n', "test-name")] + public string? TestName { get; set; } -```csharp -var options = CommandLine.Parse(args).As(); -``` + [Option("test-category")] + public string? TestCategory { get; set; } -源生成器會攔截這個調用,自動生成類似以下的代碼來替代默認通過字典查找創建器的方式實現(舊版本曾使用過反射): + [Option('d', "detail-level")] + public DetailLevel DetailLevel { get; set; } = DetailLevel.Medium; -```csharp -/// -/// 方法的攔截器。攔截以提高性能。 -/// -[global::System.Runtime.CompilerServices.InterceptsLocation(1, /* Program.Run4xInterceptor @Program.cs */ "G4GJAK7udHFnPkRUqV6VzzgRAABQcm9ncmFtLmNz")] -public static T CommandLine_As_DotNetCampusCliTestsFakesOptions(this global::DotNetCampus.Cli.CommandLine commandLine) - where T : global::DotNetCampus.Cli.Tests.Fakes.Options -{ - return (T)global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance(commandLine); + [Value(0, int.MaxValue)] + public IReadOnlyList TestItems { get; init; } = null!; } ``` -### 源生成器生成的代碼示例 - -下面是一個簡單的命令行選項類型及其對應生成的源代碼示例: +對應產生的原始碼: ```csharp -// 用戶代碼中的類型 -internal record DotNet03_MixedOptions -{ - [Option] - public int Number { get; init; } - - [Option] - public required string Text { get; init; } - - [Option] - public bool Flag { get; init; } -} +// 在 AI 翻譯完成後,人類將補充它。 ``` -對應生成的源: +## 效能數據 -```csharp -#nullable enable -namespace DotNetCampus.Cli.Tests; +解析空白命令列參數: -/// -/// 輔助 生成命令行選項、謂詞或處理函數的創建。 -/// -internal sealed class DotNet03_MixedOptionsBuilder -{ - public static object CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine) - { - var caseSensitive = commandLine.DefaultCaseSensitive; - var result = new global::DotNetCampus.Cli.Tests.DotNet03_MixedOptions - { - Number = commandLine.GetOption("number") ?? default, - Text = commandLine.GetOption("text") ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option '--text'. Command line: {commandLine}", "Text"), - Flag = commandLine.GetOption("flag") ?? default, - // There is no positional argument to be initialized. - }; - // There is no option to be assigned. - // There is no positional argument to be assigned. - return result; - } -} -``` +| Method | Mean | Error | StdDev | Gen0 | Allocated | +| ----------------------------- | -----------: | ---------: | ---------: | -----: | --------: | +| 'parse [] -v=4.1 -p=flexible' | 27.25 ns | 0.485 ns | 0.454 ns | 0.0143 | 240 B | +| 'parse [] -v=4.1 -p=dotnet' | 27.35 ns | 0.471 ns | 0.440 ns | 0.0143 | 240 B | +| 'parse [] -v=4.0 -p=flexible' | 97.16 ns | 0.708 ns | 0.628 ns | 0.0134 | 224 B | +| 'parse [] -v=4.0 -p=dotnet' | 95.90 ns | 0.889 ns | 0.742 ns | 0.0134 | 224 B | +| 'parse [] -v=3.x -p=parser' | 49.73 ns | 0.931 ns | 0.870 ns | 0.0239 | 400 B | +| 'parse [] -v=3.x -p=runtime' | 19,304.17 ns | 194.337 ns | 162.280 ns | 0.4272 | 7265 B | -## 性能數據 - -源代碼生成器實現提供了極高的命令行解析性能: - -| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | -|---------------------------------------- |----------------:|--------------:|--------------:|----------------:|-------:|-------:|----------:| -| 'parse [] --flexible' | 39.16 ns | 0.402 ns | 0.357 ns | 39.15 ns | 0.0124 | - | 208 B | -| 'parse [] --gnu' | 38.22 ns | 0.518 ns | 0.459 ns | 38.30 ns | 0.0124 | - | 208 B | -| 'parse [] --posix' | 38.45 ns | 0.792 ns | 0.741 ns | 38.45 ns | 0.0124 | - | 208 B | -| 'parse [] --dotnet' | 42.14 ns | 0.878 ns | 2.588 ns | 42.06 ns | 0.0124 | - | 208 B | -| 'parse [] --powershell' | 38.67 ns | 0.772 ns | 1.451 ns | 38.42 ns | 0.0124 | - | 208 B | -| 'parse [] -v=3.x -p=parser' | 44.07 ns | 0.665 ns | 0.841 ns | 44.08 ns | 0.0220 | - | 368 B | -| 'parse [] -v=3.x -p=runtime' | 365.36 ns | 7.186 ns | 13.319 ns | 361.47 ns | 0.0367 | - | 616 B | -| 'parse [PS1] --flexible' | 907.15 ns | 17.887 ns | 38.504 ns | 899.46 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] --dotnet' | 969.51 ns | 18.977 ns | 31.179 ns | 964.56 ns | 0.1612 | - | 2704 B | -| 'parse [PS1] -v=3.x -p=parser' | 448.38 ns | 8.883 ns | 13.830 ns | 445.91 ns | 0.0715 | - | 1200 B | -| 'parse [PS1] -v=3.x -p=runtime' | 835.83 ns | 16.055 ns | 38.774 ns | 830.59 ns | 0.0858 | - | 1448 B | -| 'parse [CMD] --flexible' | 932.31 ns | 18.636 ns | 40.907 ns | 936.14 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] --dotnet' | 877.96 ns | 8.846 ns | 9.832 ns | 877.67 ns | 0.1612 | - | 2704 B | -| 'parse [CMD] -v=3.x -p=parser' | 438.09 ns | 8.591 ns | 11.469 ns | 433.77 ns | 0.0715 | - | 1200 B | -| 'parse [CMD] -v=3.x -p=runtime' | 822.05 ns | 16.417 ns | 25.560 ns | 811.08 ns | 0.0858 | - | 1448 B | -| 'parse [GNU] --flexible' | 880.14 ns | 17.627 ns | 36.794 ns | 878.35 ns | 0.1574 | - | 2648 B | -| 'parse [GNU] --gnu' | 811.59 ns | 13.691 ns | 20.492 ns | 805.61 ns | 0.1554 | - | 2608 B | -| 'parse [GNU] -v=3.x -p=parser' | 492.48 ns | 9.757 ns | 11.615 ns | 491.95 ns | 0.0896 | - | 1512 B | -| 'parse [GNU] -v=3.x -p=runtime' | 873.40 ns | 15.873 ns | 24.713 ns | 865.86 ns | 0.1049 | - | 1760 B | -| 'handle [Edit,Print] --flexible' | 693.30 ns | 13.894 ns | 28.066 ns | 681.77 ns | 0.2375 | 0.0019 | 3984 B | -| 'handle [Edit,Print] -v=3.x -p=parser' | 949.15 ns | 18.959 ns | 25.952 ns | 939.97 ns | 0.2775 | 0.0038 | 4648 B | -| 'handle [Edit,Print] -v=3.x -p=runtime' | 6,232.90 ns | 122.601 ns | 217.924 ns | 6,190.80 ns | 0.2594 | - | 4592 B | -| 'parse [URL]' | 2,942.05 ns | 54.322 ns | 76.152 ns | 2,926.04 ns | 0.4578 | - | 7704 B | -| 'parse [URL] -v=3.x -p=parser' | 121.43 ns | 2.457 ns | 5.496 ns | 121.10 ns | 0.0440 | - | 736 B | -| 'parse [URL] -v=3.x -p=runtime' | 462.92 ns | 9.017 ns | 10.023 ns | 464.26 ns | 0.0587 | - | 984 B | -| 'NuGet: CommandLineParser' | 212,745.53 ns | 4,237.822 ns | 11,384.635 ns | 211,418.82 ns | 5.3711 | - | 90696 B | -| 'NuGet: System.CommandLine' | 1,751,023.59 ns | 34,134.634 ns | 50,034.108 ns | 1,727,339.45 ns | 3.9063 | - | 84138 B | +解析 GNU 風格命令列參數: + +```bash +test DotNetCampus.CommandLine.Performance.dll DotNetCampus.CommandLine.Sample.dll DotNetCampus.CommandLine.Test.dll -c 20 --test-name BenchmarkTest --detail-level High --debug +``` + +| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated | +| -------------------------------- | ------------- | ------------- | ----------: | --------: | --------: | -----: | --------: | +| 'parse [GNU] -v=4.1 -p=flexible' | .NET 10.0 | .NET 10.0 | 355.9 ns | 4.89 ns | 4.58 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.1 -p=gnu' | .NET 10.0 | .NET 10.0 | 339.7 ns | 6.81 ns | 7.57 ns | 0.0548 | 920 B | +| 'parse [GNU] -v=4.0 -p=flexible' | .NET 10.0 | .NET 10.0 | 945.9 ns | 14.87 ns | 13.19 ns | 0.1583 | 2656 B | +| 'parse [GNU] -v=4.0 -p=gnu' | .NET 10.0 | .NET 10.0 | 882.1 ns | 11.30 ns | 10.57 ns | 0.1631 | 2736 B | +| 'parse [GNU] -v=3.x -p=parser' | .NET 10.0 | .NET 10.0 | 495.7 ns | 9.26 ns | 9.09 ns | 0.1040 | 1752 B | +| 'parse [GNU] -v=3.x -p=runtime' | .NET 10.0 | .NET 10.0 | 18,025.5 ns | 194.73 ns | 162.61 ns | 0.4883 | 8730 B | +| 'NuGet: ConsoleAppFramework' | .NET 10.0 | .NET 10.0 | 134.1 ns | 2.70 ns | 2.65 ns | 0.0215 | 360 B | +| 'parse [GNU] -v=4.1 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 624.3 ns | 7.06 ns | 6.60 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.1 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 600.3 ns | 6.72 ns | 6.28 ns | 0.0505 | 856 B | +| 'parse [GNU] -v=4.0 -p=flexible' | NativeAOT 9.0 | NativeAOT 9.0 | 1,395.6 ns | 20.43 ns | 19.11 ns | 0.1507 | 2529 B | +| 'parse [GNU] -v=4.0 -p=gnu' | NativeAOT 9.0 | NativeAOT 9.0 | 1,438.1 ns | 19.84 ns | 18.55 ns | 0.1545 | 2609 B | +| 'parse [GNU] -v=3.x -p=parser' | NativeAOT 9.0 | NativeAOT 9.0 | 720.8 ns | 7.47 ns | 6.99 ns | 0.1030 | 1737 B | +| 'parse [GNU] -v=3.x -p=runtime' | NativeAOT 9.0 | NativeAOT 9.0 | NA | NA | NA | NA | NA | +| 'NuGet: ConsoleAppFramework' | NativeAOT 9.0 | NativeAOT 9.0 | 195.3 ns | 3.76 ns | 3.69 ns | 0.0234 | 392 B | 其中: -1. `parse` 表示調用的是 `CommandLine.Parse` 方法 -2. `handle` 表示調用的是 `CommandLine.AddHandler` 方法 -3. 中括號 `[Xxx]` 表示傳入的命令行參數的風格 -4. `--flexible` `--gnu` 等表示解析傳入命令行時所使用的解析器風格(相匹配時效率最高) -5. `-v=3.x -p=parser` 表示舊版本手工編寫解析器並傳入時的性能(性能最好,不過舊版本支持的命令行規範較少,很多合法的命令寫法並不支持) -6. `-v=3.x -p=runtime` 表示舊版本使用默認的反射解析器時的性能 -7. `NuGet: CommandLineParser` 和 `NuGet: System.CommandLine` 表示使用對應名稱的 NuGet 包解析命令行參數時的性能 -8. `parse [URL]` 表示解析 URL 協議字符串時的性能 - -新版本得益於源生成器和攔截器: -1. 完成一次解析大約在 0.8μs(微秒)左右(Benchmark) -2. 在應用程序啟動期間,完成一次解析只需要大約 34μs -3. 在應用程序啟動期間,包含dll加載、類型初始化在內的解析一次大約8ms(使用 AOT 編譯能重新降至 34μs)。 + +1. `parse` 表示呼叫 `CommandLine.Parse` +2. `handle` 表示呼叫 `CommandLine.AddHandler` +3. 中括號 `[Xxx]` 表示傳入參數風格 +4. `--flexible`、`--gnu` 等表示解析使用的風格(匹配效率最高) +5. `-v=3.x -p=parser` 為舊版手寫解析器效能(最佳但語法支援少) +6. `-v=3.x -p=runtime` 為舊版反射解析器效能 +7. `-v=4.0` 與 `-v=4.1` 顯示版本效能演進 +8. `NuGet: ...` 為其他程式庫效能 +9. `parse [URL]`(本文省略部分)為解析 URL 協議效能 + +作者觀察(@walterlv): + +1. 最快的是 [ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework);本庫性能非常接近,同量級。 +2. 感謝其對零依賴、零配置、零反射、零分配的極致追求,激勵我們完成目前版本(`-v4.1`)。 +3. 它主打極致性能,犧牲部分語法支援;我們主打「全功能 + 高性能」,因此位於同級別,很難超越它。依你的受眾與需求選擇適用方案。 From a0861a433377983d943f0ade8135f19537477a9f Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 19 Sep 2025 21:00:00 +0800 Subject: [PATCH 075/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DotNetCampus.CommandLine.Performance.csproj | 2 +- .../ParseArgs/ParseGnuArgs.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj index e905b5f3..a2f53c9a 100644 --- a/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj +++ b/tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj @@ -5,7 +5,7 @@ net8.0 DotNetCampus.Cli.Performance - + false diff --git a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs index f1482607..0412588f 100644 --- a/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs +++ b/tests/DotNetCampus.CommandLine.Performance/ParseArgs/ParseGnuArgs.cs @@ -75,7 +75,7 @@ public void ConsoleAppFramework() [Benchmark(Description = "NuGet: CommandLineParser")] public void CommandLineParser() { - Parser.Default.ParseArguments(GnuArgs).WithParsed(options => { }); + Parser.Default.ParseArguments(GnuArgs).WithParsed(options => { }); } [Benchmark(Description = "NuGet: System.CommandLine")] From 331912e1e37354ab6539f1dc1a11ccbe85ecdbde Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 09:14:05 +0800 Subject: [PATCH 076/193] =?UTF-8?q?=E5=86=8D=E4=BF=AE=E5=A4=8D=E6=9E=84?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dotnet-build.yml | 1 + .github/workflows/nuget-tag-publish.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index d6f41990..2736b589 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -13,6 +13,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | + 8.0.x 9.0.x - name: Build diff --git a/.github/workflows/nuget-tag-publish.yml b/.github/workflows/nuget-tag-publish.yml index 88359c5e..46729605 100644 --- a/.github/workflows/nuget-tag-publish.yml +++ b/.github/workflows/nuget-tag-publish.yml @@ -17,6 +17,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | + 8.0.x 9.0.x - name: Install dotnet tool From 9675cd4de7b5533c38a503b6ba6dad91cfdcb332 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 09:17:36 +0800 Subject: [PATCH 077/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=BD=9C=E4=B8=BA?= =?UTF-8?q?=20AI=20=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/README.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md new file mode 100644 index 00000000..a1294fa5 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md @@ -0,0 +1,80 @@ +# 规则一览 + +## 命令行风格 + +```csharp +// 使用 .NET CLI 风格解析命令行参数 +var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); +``` + +支持的风格包括: + +- `CommandLineStyle.Flexible`(默认):灵活风格,在各种风格间提供最大的兼容性,默认大小写不敏感 +- `CommandLineStyle.DotNet`:.NET CLI 风格,默认大小写敏感 +- `CommandLineStyle.Gnu`:符合 GNU 规范的风格,默认大小写敏感 +- `CommandLineStyle.Posix`:符合 POSIX 规范的风格,默认大小写敏感 +- `CommandLineStyle.PowerShell`:PowerShell 风格,默认大小写不敏感 + +默认情况下,这些风格的详细区别如下: + +| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ---------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | +| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | +| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | +| 选项值 `=` | -o=value | -o=value | -o=value | | | option=value | +| 选项值 `:` | -o:value | -o:value | | | | | +| 选项值 ` ` | -o value | -o value | -o value | -o value | -o value | | +| 布尔选项 | -o | -o | -o | -o | -o | option | +| 布尔选项 | -o=true | -o=true | | | -o:true | option=true | +| 布尔值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布尔值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布尔值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布尔值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | +| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | +| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | +| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 多短布尔选项合并 | 不支持 | 不支持 | -abc | -abc | 不支持 | 不支持 | +| 单短选项多字符 | -ab | -ab | 不支持 | 不支持 | -ab | 不支持 | +| 短选项直接带值 | 不支持 | 不支持 | -o1.txt | 不支持 | 不支持 | 不支持 | +| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | +| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | + +## 必需选项与默认值 + +当你定义一个属性的时候,有这些标记可用: + +1. 使用 `required` 标记一个选项是必须的 +1. 使用 `init` 标记一个选项是不可变的 +1. 使用 `?` 标记一个选项是可空的 + +而具体会被赋成什么值取决于以下这些因素: + +| required | init | 集合属性 | nullable | 行为 | 解释 | +| -------- | ---- | -------- | -------- | -------- | ----------------------------------- | +| 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | +| 0 | 1 | 1 | _ | 空集合 | 集合永不为 `null`,没传就赋值空集合 | +| 0 | 1 | 0 | 1 | `null` | 可空,没有传就赋值 `null` | +| 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | +| 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | + +- 1 = 标记了 +- 0 = 没标记 +- _ = 无论有没有标记 + +1. 可空,无论是引用类型还是值类型,其行为完全一致。要硬说不同,就是那个「默认值」会导致引用类型得到 `null`。 +2. 如果未提供必需选项,解析时会抛出`RequiredPropertyNotAssignedException`异常。 +3. 上述行为的「保留初值」的意思是,你可以在定义这个属性的时候写一个初值,就像下面这样: + +```csharp +// 请注意,这里的初值仅在没有 required 也没有 init 时才生效。 +[Option('o', "option-name")] +public string OptionName { get; set; } = "Default Value" +``` From 97751fd91885f2b52d6f544d0295e1fb07fbb2ef Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 10:23:23 +0800 Subject: [PATCH 078/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B4=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E7=9A=84=E8=A7=84=E5=88=99=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandLineParsingOptions.cs | 17 ++++ .../Exceptions/CommandLineParseException.cs | 28 +++++- .../Utils/Parsers/CommandLineParsingResult.cs | 42 ++++++-- .../CommandLineStyleTestingExtensions.cs | 37 +++++++ .../OptionValueSeparatorTests.cs | 96 +++++++++++++++++++ 5 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/CommandLineStyleTestingExtensions.cs create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs diff --git a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs index 4f43a17f..f1125753 100644 --- a/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs +++ b/src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs @@ -13,6 +13,7 @@ public readonly record struct CommandLineParsingOptions { Style = new CommandLineStyleDetails(FlexibleMagic) { + Name = "Flexible", OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -20,6 +21,7 @@ public readonly record struct CommandLineParsingOptions private static CommandLineStyleDetails FlexibleDefinition => new CommandLineStyleDetails { + Name = "Flexible", CaseSensitive = false, SupportsLongOption = true, SupportsShortOption = true, @@ -37,6 +39,7 @@ public readonly record struct CommandLineParsingOptions { Style = new CommandLineStyleDetails(DotNetMagic) { + Name = "DotNet", OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -44,6 +47,7 @@ public readonly record struct CommandLineParsingOptions private static CommandLineStyleDetails DotNetDefinition => new CommandLineStyleDetails { + Name = "DotNet", CaseSensitive = true, SupportsLongOption = true, SupportsShortOption = true, @@ -61,6 +65,7 @@ public readonly record struct CommandLineParsingOptions { Style = new CommandLineStyleDetails(GnuMagic) { + Name = "Gnu", OptionValueSeparators = CommandSeparatorChars.Create('='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -68,6 +73,7 @@ public readonly record struct CommandLineParsingOptions private static CommandLineStyleDetails GnuDefinition => new CommandLineStyleDetails { + Name = "Gnu", CaseSensitive = true, SupportsLongOption = true, SupportsShortOption = true, @@ -85,6 +91,7 @@ public readonly record struct CommandLineParsingOptions { Style = new CommandLineStyleDetails(PosixMagic) { + Name = "Posix", OptionValueSeparators = CommandSeparatorChars.Create(), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -92,6 +99,7 @@ public readonly record struct CommandLineParsingOptions private static CommandLineStyleDetails PosixDefinition => new CommandLineStyleDetails { + Name = "Posix", CaseSensitive = true, SupportsLongOption = false, SupportsShortOption = true, @@ -111,6 +119,7 @@ public readonly record struct CommandLineParsingOptions { Style = new CommandLineStyleDetails(PowerShellMagic) { + Name = "PowerShell", OptionValueSeparators = CommandSeparatorChars.Create(':', '='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }, @@ -119,6 +128,7 @@ public readonly record struct CommandLineParsingOptions /// private static CommandLineStyleDetails PowerShellDefinition => new CommandLineStyleDetails { + Name = "PowerShell", CaseSensitive = false, SupportsLongOption = true, SupportsShortOption = true, @@ -136,6 +146,7 @@ public readonly record struct CommandLineParsingOptions ///
public static CommandLineStyleDetails UrlStyle => new CommandLineStyleDetails(UrlMagic) { + Name = "Url", OptionValueSeparators = CommandSeparatorChars.Create('='), CollectionValueSeparators = CommandSeparatorChars.Create(',', ';'), }; @@ -145,6 +156,7 @@ public readonly record struct CommandLineParsingOptions ///
private static CommandLineStyleDetails UrlDefinition => new CommandLineStyleDetails { + Name = "Url", CaseSensitive = false, SupportsLongOption = true, SupportsShortOption = false, @@ -406,6 +418,11 @@ public bool SupportsSpaceSeparatedCollectionValues ///
public CommandSeparatorChars CollectionValueSeparators { get; init; } + /// + /// 此命令行风格的名称,用于调试和日志记录。 + /// + public string Name { get; init; } = "Custom"; + /// /// 获取用于存储样式细节的魔术数字。 /// diff --git a/src/DotNetCampus.CommandLine/Exceptions/CommandLineParseException.cs b/src/DotNetCampus.CommandLine/Exceptions/CommandLineParseException.cs index f78ca510..dfcfe59f 100644 --- a/src/DotNetCampus.CommandLine/Exceptions/CommandLineParseException.cs +++ b/src/DotNetCampus.CommandLine/Exceptions/CommandLineParseException.cs @@ -1,4 +1,6 @@ -namespace DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; + +namespace DotNetCampus.Cli.Exceptions; /// /// 在解析命令行参数的过程中发生的异常。 @@ -7,6 +9,11 @@ public class CommandLineParseException : CommandLineException { private const string DefaultMessage = "Parse the command line failed."; + /// + /// 获取导致异常的命令行解析错误类型。 + /// + public CommandLineParsingError Reason { get; } + /// /// 初始化 类的新实例。 /// @@ -22,6 +29,16 @@ public CommandLineParseException(string message) : base(message) { } + /// + /// 初始化 类的新实例。 + /// + /// 导致异常的命令行解析错误类型。 + /// 异常消息。 + public CommandLineParseException(CommandLineParsingError reason, string message) : base(message) + { + Reason = reason; + } + /// /// 初始化 类的新实例。 /// @@ -54,6 +71,15 @@ public CommandLineParseValueException(string message) : base(message) { } + /// + /// 初始化 类的新实例。 + /// + /// 导致异常的命令行解析错误类型。 + /// 异常消息。 + public CommandLineParseValueException(CommandLineParsingError reason, string message) : base(reason, message) + { + } + /// /// 初始化 类的新实例。 /// diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs index 6d0ae50c..48d5da4d 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -43,11 +43,12 @@ public void ThrowIfError() throw ErrorType switch { - CommandLineParsingError.OptionalArgumentNotFound => new CommandLineParseException(ErrorMessage!), - CommandLineParsingError.OptionalArgumentParseError => new CommandLineParseException(ErrorMessage!), - CommandLineParsingError.PositionalArgumentNotFound => new CommandLineParseException(ErrorMessage!), - CommandLineParsingError.BooleanValueParseError => new CommandLineParseValueException(ErrorMessage!), - CommandLineParsingError.DictionaryValueParseError => new CommandLineParseValueException(ErrorMessage!), + CommandLineParsingError.OptionalArgumentNotFound => new CommandLineParseException(ErrorType, ErrorMessage!), + CommandLineParsingError.OptionalArgumentSeparatorNotSupported => new CommandLineParseException(ErrorType, ErrorMessage!), + CommandLineParsingError.OptionalArgumentParseError => new CommandLineParseException(ErrorType, ErrorMessage!), + CommandLineParsingError.PositionalArgumentNotFound => new CommandLineParseException(ErrorType, ErrorMessage!), + CommandLineParsingError.BooleanValueParseError => new CommandLineParseValueException(ErrorType, ErrorMessage!), + CommandLineParsingError.DictionaryValueParseError => new CommandLineParseValueException(ErrorType, ErrorMessage!), CommandLineParsingError.None => throw new CommandLineException("解析过程中没有发生任何错误。"), _ => throw new CommandLineException("未知的命令行解析错误类型。"), }; @@ -75,8 +76,14 @@ public void ThrowIfError() /// 表示选项未找到的解析结果。 public static CommandLineParsingResult OptionalArgumentNotFound(CommandLine commandLine, int index, string commandObjectName, ReadOnlySpan optionName) { - var message = $"命令行对象 {commandObjectName} 不包含选项 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; - return new CommandLineParsingResult(CommandLineParsingError.OptionalArgumentNotFound, message); + var possibleSeparatorIndex = optionName.IndexOfAnyNonLetterOrDigit(); + var reason = possibleSeparatorIndex >= 0 + ? CommandLineParsingError.OptionalArgumentSeparatorNotSupported + : CommandLineParsingError.OptionalArgumentNotFound; + var message = possibleSeparatorIndex >= 0 + ? $"当前解析选项 {commandLine.ParsingOptions.Style.Name} 不支持选项值分隔符 '{optionName[possibleSeparatorIndex]}',因此无法识别参数 {commandLine.CommandLineArguments[index]}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。" + : $"命令行对象 {commandObjectName} 没有任何属性的选项名为 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; + return new CommandLineParsingResult(reason, message); } /// @@ -131,6 +138,22 @@ public static CommandLineParsingResult DictionaryValueParseError(CommandLine com } } +file static class Extensions +{ + internal static int IndexOfAnyNonLetterOrDigit(this ReadOnlySpan span) + { + for (var i = 0; i < span.Length; i++) + { + if (!char.IsLetterOrDigit(span[i])) + { + return i; + } + } + + return -1; + } +} + /// /// 命令行参数解析错误类型。 /// @@ -146,6 +169,11 @@ public enum CommandLineParsingError : byte /// OptionalArgumentNotFound, + /// + /// 没有任何选项能够匹配当前的命令行参数,可能是因为当前的命令行参数使用了不被支持的选项值分隔符。 + /// + OptionalArgumentSeparatorNotSupported, + /// /// 当前的命令行参数无法解析出选项名。 /// diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/CommandLineStyleTestingExtensions.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/CommandLineStyleTestingExtensions.cs new file mode 100644 index 00000000..adeefd2d --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/CommandLineStyleTestingExtensions.cs @@ -0,0 +1,37 @@ +using System; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +internal static class CommandLineStyleTestingExtensions +{ + public static CommandLineParsingOptions ToParsingOptions(this CommandLineStyle style) => style switch + { + CommandLineStyle.Flexible => CommandLineParsingOptions.Flexible, + CommandLineStyle.DotNet => CommandLineParsingOptions.DotNet, + CommandLineStyle.Gnu => CommandLineParsingOptions.Gnu, + CommandLineStyle.Posix => CommandLineParsingOptions.Posix, + CommandLineStyle.PowerShell => CommandLineParsingOptions.PowerShell, + _ => throw new ArgumentOutOfRangeException(nameof(style), style, null), + }; + + public static CommandLineParsingOptions ToParsingOptions(this TestCommandLineStyle style) => style switch + { + TestCommandLineStyle.Flexible => CommandLineParsingOptions.Flexible, + TestCommandLineStyle.DotNet => CommandLineParsingOptions.DotNet, + TestCommandLineStyle.Gnu => CommandLineParsingOptions.Gnu, + TestCommandLineStyle.Posix => CommandLineParsingOptions.Posix, + TestCommandLineStyle.PowerShell => CommandLineParsingOptions.PowerShell, + TestCommandLineStyle.Url => CommandLineParsingOptions.Flexible with { SchemeNames = ["test"] }, + _ => throw new ArgumentOutOfRangeException(nameof(style), style, null), + }; +} + +public enum TestCommandLineStyle +{ + Flexible, + DotNet, + Gnu, + Posix, + PowerShell, + Url, +} diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs new file mode 100644 index 00000000..e4ff8dbe --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -0,0 +1,96 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class OptionValueSeparatorTests +{ + [TestMethod] + // option=value + [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=value")] + [DataRow(new[] { "--option=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=value")] + [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=value")] + [DataRow(new[] { "-Option=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=value")] + [DataRow(new[] { "-option=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option=value")] + [DataRow(new[] { "/Option=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option=value")] + [DataRow(new[] { "/option=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option=value")] + [DataRow(new[] { "test://?option=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option=value")] + // o=value + [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=value")] + [DataRow(new[] { "-o=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=value")] + [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o=value")] + [DataRow(new[] { "-o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=value")] + [DataRow(new[] { "/o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o=value")] + // option:value + [DataRow(new[] { "--option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option:value")] + [DataRow(new[] { "--option:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option:value")] + [DataRow(new[] { "-Option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option:value")] + [DataRow(new[] { "-option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option:value")] + [DataRow(new[] { "/Option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option:value")] + [DataRow(new[] { "/option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option:value")] + // option value + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option value")] + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option value")] + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option value")] + [DataRow(new[] { "-Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option value")] + [DataRow(new[] { "-option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option value")] + [DataRow(new[] { "/Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option value")] + [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option value")] + public void Supported(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.Option); + } + + [TestMethod] + // option=value + [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Posix, DisplayName = "[Posix] --option=value")] + // o=value + [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Posix, DisplayName = "[Posix] -o=value")] + // option:value + [DataRow(new[] { "--option:value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option:value")] + [DataRow(new[] { "test://?option:value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option:value")] + // option value + [DataRow(new[] { "test://?option%20value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option value")] + public void NotSupported(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.OptionalArgumentSeparatorNotSupported, exception.Reason); + } + + [TestMethod] + // o=value + [DataRow(new[] { "test://?o=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] o=value")] + public void ShorOptionNotSupported(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); + } + + public record TestOptions + { + [Option('o', "option")] + public string? Option { get; set; } + } +} From 7963d7ff7d6cc6785a25badb34959ed306ec0b83 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 12:37:41 +0800 Subject: [PATCH 079/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=8C=E5=B9=B6=E5=90=8C=E6=97=B6=E5=8F=91?= =?UTF-8?q?=E7=8E=B0=E4=BA=86=20GNU=20=E8=A7=84=E5=88=99=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/CommandSeparatorChars.cs | 41 +++++++++++++++++++ .../Utils/Parsers/CommandLineParser.cs | 31 ++++---------- .../Utils/Parsers/CommandLineParsingResult.cs | 5 ++- .../OptionValueSeparatorTests.cs | 40 ++++++++++++++---- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs b/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs index aafefbbe..87ad0c5f 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandSeparatorChars.cs @@ -11,6 +11,11 @@ namespace DotNetCampus.Cli.Utils; #endif public readonly record struct CommandSeparatorChars : IEnumerable { + /// + /// 获取一个空的分隔符字符集合实例。 + /// + public static CommandSeparatorChars Empty => new CommandSeparatorChars('\0', '\0'); + /// /// 分隔符字符集合中允许的最大字符数量。 /// @@ -26,6 +31,24 @@ private CommandSeparatorChars(char char0, char char1) _char1 = char1; } + /// + /// 返回指定文本中第一个分隔符字符的索引;如果未找到任何分隔符字符,则返回 -1。 + /// + /// 要搜索的文本。 + /// 第一个分隔符字符的索引;如果未找到任何分隔符字符,则返回 -1。 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int SeparateIndex(ReadOnlySpan text) + { + foreach (var c in text) + { + if (c == _char0 || c == _char1) + { + return text.IndexOf(c); + } + } + return -1; + } + /// /// 以只读列表形式返回分隔符字符集合。 /// @@ -82,3 +105,21 @@ public IEnumerator GetEnumerator() _ => throw new ArgumentOutOfRangeException(nameof(chars), $"The length of chars cannot be greater than {MaxSupportedCount}."), }; } + +/// +/// 的扩展方法。 +/// +public static class CommandSeparatorCharsExtensions +{ + /// + /// 返回指定文本中第一个分隔符字符的索引;如果未找到任何分隔符字符,则返回 -1。 + /// + /// 要搜索的文本。 + /// 分隔符字符集合。 + /// 第一个分隔符字符的索引;如果未找到任何分隔符字符,则返回 -1。 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfAny(this ReadOnlySpan span, CommandSeparatorChars separatorChars) + { + return separatorChars.SeparateIndex(span); + } +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 7f58859e..5c178cbe 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -272,10 +272,7 @@ private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadO var result = CommandLineParsingResult.Success; if (match.ValueType is OptionValueType.List) { - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - Style.CollectionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; - + var separators = Style.CollectionValueSeparators; var start = 0; while (start < value.Length) { @@ -297,10 +294,7 @@ private CommandLineParsingResult AssignOptionValue(OptionValueMatch match, ReadO } else if (match.ValueType is OptionValueType.Dictionary) { - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - Style.CollectionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; - + var separators = Style.CollectionValueSeparators; var start = 0; while (start < value.Length) { @@ -541,9 +535,7 @@ private bool ParseOptionOrPositionalArgument() if (argument.Length is 1) { // 单个字符,确定一下是否是选项分隔符,如果是则要报错。 - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; + var separators = _parser.Style.OptionValueSeparators; if (argument.IndexOfAny(separators) >= 0) { // 仅包含分隔符,视为错误选项。 @@ -591,10 +583,7 @@ private bool ParseOptionOrPositionalArgument() private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) { - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; - + var separators = _parser.Style.OptionValueSeparators; var index = argument.IndexOfAny(separators); if (index is 0) { @@ -618,10 +607,9 @@ private bool ParseLongOptionOrLongOptionWithValue(ReadOnlySpan argument) private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) { - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; - + var separators = _parser.SupportsShortOptionValueWithoutSeparator + ? CommandSeparatorChars.Empty + : _parser.Style.OptionValueSeparators; var index = argument.IndexOfAny(separators); if (index is 0) { @@ -659,10 +647,7 @@ private bool ParseShortOptionOrMultiShortOptions(ReadOnlySpan argument) private bool ParseLongShortOptionOrLongShortOptionWithValue(ReadOnlySpan argument) { - Span separators = stackalloc char[CommandSeparatorChars.MaxSupportedCount]; - _parser.Style.OptionValueSeparators.CopyTo(separators, out var length); - separators = separators[..length]; - + var separators = _parser.Style.OptionValueSeparators; var index = argument.IndexOfAny(separators); if (index is 0) { diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs index 48d5da4d..b92e0ad4 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -80,9 +80,12 @@ public static CommandLineParsingResult OptionalArgumentNotFound(CommandLine comm var reason = possibleSeparatorIndex >= 0 ? CommandLineParsingError.OptionalArgumentSeparatorNotSupported : CommandLineParsingError.OptionalArgumentNotFound; + var isUrl = commandLine.MatchedUrlScheme is not null; var message = possibleSeparatorIndex >= 0 ? $"当前解析选项 {commandLine.ParsingOptions.Style.Name} 不支持选项值分隔符 '{optionName[possibleSeparatorIndex]}',因此无法识别参数 {commandLine.CommandLineArguments[index]}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。" - : $"命令行对象 {commandObjectName} 没有任何属性的选项名为 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; + : isUrl + ? $"命令行对象 {commandObjectName} 没有任何属性的选项名为 {optionName.ToString()},请注意解析 URL 时不支持短选项参数。URL={commandLine.ToRawString()}" + : $"命令行对象 {commandObjectName} 没有任何属性的选项名为 {optionName.ToString()}。参数列表:{commandLine},索引 {index},参数 {commandLine.CommandLineArguments[index]}。"; return new CommandLineParsingResult(reason, message); } diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs index e4ff8dbe..297cee08 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -21,7 +21,6 @@ public class OptionValueSeparatorTests // o=value [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=value")] [DataRow(new[] { "-o=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=value")] - [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o=value")] [DataRow(new[] { "-o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=value")] [DataRow(new[] { "/o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o=value")] // option:value @@ -31,6 +30,11 @@ public class OptionValueSeparatorTests [DataRow(new[] { "-option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option:value")] [DataRow(new[] { "/Option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option:value")] [DataRow(new[] { "/option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option:value")] + // o:value + [DataRow(new[] { "-o:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o:value")] + [DataRow(new[] { "-o:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o:value")] + [DataRow(new[] { "-o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o:value")] + [DataRow(new[] { "/o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o:value")] // option value [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option value")] [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option value")] @@ -39,6 +43,12 @@ public class OptionValueSeparatorTests [DataRow(new[] { "-option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option value")] [DataRow(new[] { "/Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option value")] [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option value")] + // o value + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o value")] + [DataRow(new[] { "/o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o value")] public void Supported(string[] args, TestCommandLineStyle style) { // Arrange @@ -52,15 +62,11 @@ public void Supported(string[] args, TestCommandLineStyle style) } [TestMethod] - // option=value - [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Posix, DisplayName = "[Posix] --option=value")] - // o=value - [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Posix, DisplayName = "[Posix] -o=value")] - // option:value [DataRow(new[] { "--option:value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option:value")] [DataRow(new[] { "test://?option:value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option:value")] - // option value + [DataRow(new[] { "test://?o:value" }, TestCommandLineStyle.Url, DisplayName = "[Url] o:value")] [DataRow(new[] { "test://?option%20value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option value")] + [DataRow(new[] { "test://?o%20value" }, TestCommandLineStyle.Url, DisplayName = "[Url] o value")] public void NotSupported(string[] args, TestCommandLineStyle style) { // Arrange @@ -73,10 +79,25 @@ public void NotSupported(string[] args, TestCommandLineStyle style) Assert.AreEqual(CommandLineParsingError.OptionalArgumentSeparatorNotSupported, exception.Reason); } + + [TestMethod] + [DataRow(new[] { "-o=value" }, "=value", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o=value (预期值为 '=value')")] + [DataRow(new[] { "-o:value" }, ":value", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o:value (预期值为 ':value')")] + public void GnuDoesNotSupportShortOptionSeparator(string[] args, string value, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual(value, options.Option); + } + [TestMethod] - // o=value [DataRow(new[] { "test://?o=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] o=value")] - public void ShorOptionNotSupported(string[] args, TestCommandLineStyle style) + public void UrlStyleDoesNotSupportShortOption(string[] args, TestCommandLineStyle style) { // Arrange var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); @@ -86,6 +107,7 @@ public void ShorOptionNotSupported(string[] args, TestCommandLineStyle style) // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); + Assert.Contains("URL", exception.Message); } public record TestOptions From 600ab43c2c0794b0f5fbad4872f9616fc1e240c4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 13:35:41 +0800 Subject: [PATCH 080/193] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AF=B9=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=9E=8B=E6=8A=A5=E5=91=8A=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnalyzerReleases.Shipped.md | 1 + .../Diagnostics.cs | 15 +++++++++++ .../Generators/ModelBuilderGenerator.cs | 15 ++++++++++- .../Models/CommandObjectGeneratingModel.cs | 8 +++++- .../Properties/Localizations.Designer.cs | 27 +++++++++++++++++++ .../Properties/Localizations.resx | 9 +++++++ .../Properties/Localizations.zh-hans.resx | 9 +++++++ 7 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md index 78649d82..dc0b2513 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md +++ b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md @@ -11,6 +11,7 @@ DCL102 | DotNetCampus.AvoidBugs | Info | OptionalArgumentSeparatorNotSupported, + /// + /// 当前命令行风格不支持多字符短选项。 + /// + MultiCharShortOptionalArgumentNotSupported, + /// /// 当前的命令行参数正试图使用短布尔选项组合的方式来表示一个非布尔类型的选项。 /// diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs index 861450f2..6791d5f5 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs @@ -153,6 +153,39 @@ public void DoesNotSupportBooleanOptionCombination(string[] args, TestCommandLin Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); } + [TestMethod] + [DataRow(new[] { "-ab" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab")] + [DataRow(new[] { "-ab" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab")] + [DataRow(new[] { "-ab" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab")] + public void SupportMultiCharShortOptions(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsTrue(options.OptionA); + Assert.IsNull(options.OptionB); + } + + [TestMethod] + [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] + [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + public void MultiCharShortOptionsDoesNotSupportValue(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + public record TestOptions { [Option('o', "option")] @@ -170,4 +203,13 @@ public record TestCombinationOptions [Option('c', "option-c")] public string? OptionC { get; set; } } + + public record MultiCharShortOptions + { + [Option("ab", "option-ab")] + public bool? OptionA { get; set; } + + [Option('b', "option-b")] + public bool? OptionB { get; set; } + } } diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs index 12b7c6b8..50a4cc33 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -9,6 +9,20 @@ namespace DotNetCampus.Cli.Tests.ParsingStyles; public class OptionValueSeparatorTests { [TestMethod] + // option value + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option value")] + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option value")] + [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option value")] + [DataRow(new[] { "-Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option value")] + [DataRow(new[] { "-option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option value")] + [DataRow(new[] { "/Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option value")] + [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option value")] + // o value + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o value")] + [DataRow(new[] { "/o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o value")] // option=value [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=value")] [DataRow(new[] { "--option=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=value")] @@ -35,20 +49,6 @@ public class OptionValueSeparatorTests [DataRow(new[] { "-o:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o:value")] [DataRow(new[] { "-o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o:value")] [DataRow(new[] { "/o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o:value")] - // option value - [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option value")] - [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option value")] - [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option value")] - [DataRow(new[] { "-Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option value")] - [DataRow(new[] { "-option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option value")] - [DataRow(new[] { "/Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /Option value")] - [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option value")] - // o value - [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o value")] - [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value")] - [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o value")] - [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o value")] - [DataRow(new[] { "/o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o value")] // ovalue [DataRow(new[] { "-ovalue" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -ovalue")] public void Supported(string[] args, TestCommandLineStyle style) @@ -133,6 +133,12 @@ public void UrlStyleDoesNotSupportShortOption(string[] args, TestCommandLineStyl [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] + [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] + [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] public void SupportMultiCharShortOptions(string[] args, TestCommandLineStyle style) { // Arrange @@ -160,6 +166,21 @@ public void DoesNotSupportMultiCharShortOptions(string[] args, TestCommandLineSt Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); } + [TestMethod] + [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.Gnu, DisplayName = "[Flexible] -ab value")] + public void DoesNotSupportMultiCharShortOptionsWithValue(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.MultiCharShortOptionalArgumentNotSupported, exception.Reason); + } + public record TestOptions { [Option('o', "option")] From 1bb6b952cb57e03ce82dee47194689128ad94e26 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 18:02:58 +0800 Subject: [PATCH 087/193] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionCollectionValueTests.cs | 130 ++++++++++++++++++ .../OptionValueSeparatorTests.cs | 5 +- 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs new file mode 100644 index 00000000..fff800f8 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs @@ -0,0 +1,130 @@ +using System.Collections; +using System.Collections.Generic; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class OptionCollectionValueTests +{ + [TestMethod] + // option a option b + [DataRow(new[] { "--option", "a", "--option", "b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option a --option b")] + [DataRow(new[] { "--option", "a", "--option", "b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option a --option b")] + [DataRow(new[] { "--option", "a", "--option", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a --option b")] + [DataRow(new[] { "-Option", "a", "-Option", "b" }, TestCommandLineStyle.PowerShell, DisplayName = "[Gnu] -Option a -Option b")] + [DataRow(new[] { "-o", "a", "-o", "b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a -o b")] + [DataRow(new[] { "-o", "a", "-o", "b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a -o b")] + [DataRow(new[] { "-o", "a", "-o", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a -o b")] + [DataRow(new[] { "-o", "a", "-o", "b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o a -o b")] + // option a b + [DataRow(new[] { "--option", "a", "b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option a b")] + [DataRow(new[] { "--option", "a", "b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option a b")] + [DataRow(new[] { "-Option", "a", "b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option a b")] + [DataRow(new[] { "-o", "a", "b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a b")] + [DataRow(new[] { "-o", "a", "b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a b")] + [DataRow(new[] { "-o", "a", "b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o a b")] + // option a,b + [DataRow(new[] { "--option", "a,b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option a,b")] + [DataRow(new[] { "--option", "a,b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option a,b")] + [DataRow(new[] { "--option", "a,b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a,b")] + [DataRow(new[] { "-Option", "a,b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option a,b")] + [DataRow(new[] { "-o", "a,b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a,b")] + [DataRow(new[] { "-o", "a,b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a,b")] + [DataRow(new[] { "-o", "a,b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a,b")] + [DataRow(new[] { "-o", "a,b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o a,b")] + // option a;b + [DataRow(new[] { "--option", "a;b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option a;b")] + [DataRow(new[] { "--option", "a;b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option a;b")] + [DataRow(new[] { "--option", "a;b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a;b")] + [DataRow(new[] { "-Option", "a;b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option a;b")] + [DataRow(new[] { "-o", "a;b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a;b")] + [DataRow(new[] { "-o", "a;b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a;b")] + [DataRow(new[] { "-o", "a;b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a;b")] + [DataRow(new[] { "-o", "a;b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o a;b")] + // option=a option=b + [DataRow(new[] { "--option=a", "--option=b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=a --option=b")] + [DataRow(new[] { "--option=a", "--option=b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=a --option=b")] + [DataRow(new[] { "--option=a", "--option=b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=a --option=b")] + [DataRow(new[] { "-Option=a", "-Option=b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=a -Option=b")] + [DataRow(new[] { "test://?option=a&option=b" }, TestCommandLineStyle.Url, DisplayName = "[Uri] test://?option=a&option=b")] + [DataRow(new[] { "-o=a", "-o=b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=a -o=b")] + [DataRow(new[] { "-o=a", "-o=b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=a -o=b")] + [DataRow(new[] { "-o=a", "-o=b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=a -o=b")] + // option=a,b + [DataRow(new[] { "--option=a,b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=a,b")] + [DataRow(new[] { "--option=a,b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=a,b")] + [DataRow(new[] { "--option=a,b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=a,b")] + [DataRow(new[] { "-Option=a,b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=a,b")] + [DataRow(new[] { "test://?option=a,b" }, TestCommandLineStyle.Url, DisplayName = "[Uri] test://?option=a,b")] + [DataRow(new[] { "-o=a,b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=a,b")] + [DataRow(new[] { "-o=a,b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=a,b")] + [DataRow(new[] { "-o=a,b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=a,b")] + // option=a;b + [DataRow(new[] { "--option=a;b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=a;b")] + [DataRow(new[] { "--option=a;b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=a;b")] + [DataRow(new[] { "--option=a;b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=a;b")] + [DataRow(new[] { "-Option=a;b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=a;b")] + [DataRow(new[] { "test://?option=a;b" }, TestCommandLineStyle.Url, DisplayName = "[Uri] test://?option=a;b")] + [DataRow(new[] { "-o=a;b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=a;b")] + [DataRow(new[] { "-o=a;b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=a;b")] + [DataRow(new[] { "-o=a;b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=a;b")] + // oa,b + [DataRow(new[] { "-oa,b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -oa,b")] + // oa;b + [DataRow(new[] { "-oa;b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -oa;b")] + public void Supported_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options.Option); + CollectionAssert.AreEqual(new[] { "a", "b" }, (ICollection)options.Option); + } + + [TestMethod] + // option a b + [DataRow(new[] { "--option", "a", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a b")] + [DataRow(new[] { "-o", "a", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a b")] + public void NotSupported_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + + [TestMethod] + // o=a o=b + [DataRow(new[] { "-o=a", "-o=b" }, new[] { "=a", "=b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o=a -o=b (预期值为 '=a, =b')")] + [DataRow(new[] { "-o:a", "-o:b" }, new[] { ":a", ":b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o:a -o:b (预期值为 ':a, :b')")] + public void GnuDoesNotSupportShortOptionSeparator(string[] args, string[] expectedValues, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options.Option); + CollectionAssert.AreEqual(expectedValues, (ICollection)options.Option); + } + + public record TestOptions + { + [Option('o', "option")] + public IReadOnlyList? Option { get; set; } + } +} diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs index 50a4cc33..e3bfd4a8 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -81,11 +81,10 @@ public void NotSupported(string[] args, TestCommandLineStyle style) Assert.AreEqual(CommandLineParsingError.OptionalArgumentSeparatorNotSupported, exception.Reason); } - [TestMethod] [DataRow(new[] { "-o=value" }, "=value", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o=value (预期值为 '=value')")] [DataRow(new[] { "-o:value" }, ":value", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o:value (预期值为 ':value')")] - public void GnuDoesNotSupportShortOptionSeparator(string[] args, string value, TestCommandLineStyle style) + public void GnuDoesNotSupportShortOptionSeparator(string[] args, string expectedValue, TestCommandLineStyle style) { // Arrange var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); @@ -94,7 +93,7 @@ public void GnuDoesNotSupportShortOptionSeparator(string[] args, string value, T var options = commandLine.As(); // Assert - Assert.AreEqual(value, options.Option); + Assert.AreEqual(expectedValue, options.Option); } [TestMethod] From 41163ec2d54248c4adfd3242348fa5b33f109261 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 18:42:57 +0800 Subject: [PATCH 088/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A5=87=E6=80=AA?= =?UTF-8?q?=E7=9A=84=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionCollectionValueTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs index fff800f8..8a6951b2 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs @@ -89,6 +89,22 @@ public void Supported_Collection(string[] args, TestCommandLineStyle style) CollectionAssert.AreEqual(new[] { "a", "b" }, (ICollection)options.Option); } + [TestMethod] + [DataRow(new[] { "--option", "a", "--option", "b,c" }, TestCommandLineStyle.DotNet, DisplayName = "[Flexible] --option a --option b,c")] + [DataRow(new[] { "--option", "a;b,c" }, TestCommandLineStyle.DotNet, DisplayName = "[Flexible] --option a;b,c")] + public void SupportedButStrange_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options.Option); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.Option); + } + [TestMethod] // option a b [DataRow(new[] { "--option", "a", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a b")] @@ -122,6 +138,23 @@ public void GnuDoesNotSupportShortOptionSeparator(string[] args, string[] expect CollectionAssert.AreEqual(expectedValues, (ICollection)options.Option); } + [TestMethod] + [DataRow(new[] { "--option=a", "b,c" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=a b,c")] + [DataRow(new[] { "--option=a", "b,c" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=a b,c")] + [DataRow(new[] { "--option=a", "b,c" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=a b,c")] + [DataRow(new[] { "-Option=a", "b,c" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=a b,c")] + public void DoesNotSupportOptionWithValueAndArgumentValueCollection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + public record TestOptions { [Option('o', "option")] From 326f73010954a5984df4f9030c406da7e9eda11b Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 18:51:43 +0800 Subject: [PATCH 089/193] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=AD=97=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionDictionaryValueTests.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs new file mode 100644 index 00000000..c55a75e1 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class OptionDictionaryValueTests +{ + [TestMethod] + // option key1=value1 option key2=value2 + [DataRow(new[] { "--option", "a=x", "--option", "b=y" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option a=x --option b=y")] + [DataRow(new[] { "--option", "a=x", "--option", "b=y" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option a=x --option b=y")] + [DataRow(new[] { "--option", "a=x", "--option", "b=y" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option a=x --option b=y")] + [DataRow(new[] { "-Option", "a=x", "-Option", "b=y" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option a=x -Option b=y")] + // option:key1=value1;key2=value2 + [DataRow(new[] { "--option:a=x;b=y" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option:a=x;b=y")] + [DataRow(new[] { "--option:a=x;b=y" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option:a=x;b=y")] + [DataRow(new[] { "-Option:a=x;b=y" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option:a=x;b=y")] + [DataRow(new[] { "-o:a=x;b=y" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o:a=x;b=y")] + [DataRow(new[] { "-o:a=x;b=y" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o:a=x;b=y")] + [DataRow(new[] { "-o:a=x;b=y" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o:a=x;b=y")] + public void Supported_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options.Option); + CollectionAssert.AreEquivalent(new Dictionary + { + ["a"] = "x", + ["b"] = "y", + }, (ICollection)options.Option); + } + + [TestMethod] + // option=key1=value1;key2=value2 + [DataRow(new[] { "--option=a=x;b=y" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=a=x;b=y")] + [DataRow(new[] { "--option=a=x;b=y" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=a=x;b=y")] + [DataRow(new[] { "--option=a=x;b=y" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=a=x;b=y")] + [DataRow(new[] { "-Option=a=x;b=y" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=a=x;b=y")] + public void SupportedButStrange_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options.Option); + CollectionAssert.AreEquivalent(new Dictionary + { + ["a"] = "x", + ["b"] = "y", + }, (ICollection)options.Option); + } + + public record TestOptions + { + [Option('o', "option")] + public IReadOnlyDictionary? Option { get; set; } + } +} From c0897fd9009912170242453134b8a68d7fd89377 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 19:22:01 +0800 Subject: [PATCH 090/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=97=B6=EF=BC=8C--option-name=20=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E4=B8=AD=E7=9A=84=20-=20=E8=A2=AB=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E6=88=90=E5=88=86=E9=9A=94=E7=AC=A6=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParsingResult.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs index 9f8c35b8..70edd87e 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsingResult.cs @@ -82,7 +82,7 @@ public static CommandLineParsingResult OptionalArgumentNotFound(CommandLine comm ReadOnlySpan optionName, bool? isLongOption) { var isUrl = commandLine.MatchedUrlScheme is not null; - var possibleSeparatorIndex = optionName.IndexOfAnyNonLetterOrDigit(); + var possibleSeparatorIndex = optionName.IndexOfAnyPossibleSeparators(); var reason = (isLongOption, possibleSeparatorIndex) switch { (_, < 0) => CommandLineParsingError.OptionalArgumentNotFound, @@ -183,11 +183,11 @@ public static CommandLineParsingResult DictionaryValueParseError(CommandLine com file static class Extensions { - internal static int IndexOfAnyNonLetterOrDigit(this ReadOnlySpan span) + internal static int IndexOfAnyPossibleSeparators(this ReadOnlySpan span) { for (var i = 0; i < span.Length; i++) { - if (!char.IsLetterOrDigit(span[i])) + if (!char.IsLetterOrDigit(span[i]) && span[i] is not '-' and not '_' and not '.') { return i; } From f48d6575d389ccdf24c8125734a696b75ac5c487 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 19:22:22 +0800 Subject: [PATCH 091/193] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9D=9E=20kebab-cas?= =?UTF-8?q?e=20=E5=91=BD=E5=90=8D=E6=B3=95=E4=B9=9F=E8=83=BD=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=20kebab-case=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 0c45e4e7..e66d1d0e 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -137,18 +137,19 @@ private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDec .Condition(optionProperties.Count is 0, b => b .AddRawStatement("// 没有长名称选项,无需匹配。")) .Otherwise(b => b - .AddRawStatement("// 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") - .AddBracketScope("switch (longOption)", s => s - .AddRawStatements(optionProperties.Select(x => GenerateLongOptionCaseCode(x, x.GetOrdinalLongNames())))) - .AddLineSeparator() - .AddDefaultStringComparisonIfNeeded(optionProperties) - .AddLineSeparator() - .AddRawStatement("// 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。") + .AddRawStatement("// 1. 先匹配 kebab-case 命名法(原样字符串)") .AddBracketScope("if (namingPolicy.SupportsOrdinal())", s => s + .AddRawStatement("// 1.1 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。") + .AddBracketScope("switch (longOption)", c => c + .AddRawStatements(optionProperties.Select(x => GenerateLongOptionCaseCode(x, x.GetOrdinalLongNames())))) + .AddLineSeparator() + .AddRawStatement("// 1.2 再按指定大小写匹配一遍(能应对不规范命令行大小写)。") + .AddDefaultStringComparisonIfNeeded(optionProperties) .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetOrdinalLongNames())))) .AddLineSeparator() - .AddRawStatement("// 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。") + .AddRawStatement("// 2. 再匹配其他命名法(能应对所有不规范命令行大小写,并支持所有风格)。") .AddBracketScope("if (namingPolicy.SupportsPascalCase())", s => s + .AddDefaultStringComparisonIfNeeded(optionProperties) .AddRawStatements(optionProperties.Select(x => GenerateLongOptionEqualsCode(x, x.GetPascalCaseLongNames())))) .AddLineSeparator()) .EndCondition() From 4d22b117d4790ead65945c8484ab818e3eba252b Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 19:22:34 +0800 Subject: [PATCH 092/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E6=B3=95=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DotNetCampus.CommandLine.Tests.csproj | 2 - .../ParsingStyles/OptionNamingPolicyTests.cs | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj b/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj index 1df9aa37..a41ba65d 100644 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj +++ b/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj @@ -2,9 +2,7 @@ net8.0 - false - DotNetCampus.Cli.Tests diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs new file mode 100644 index 00000000..9131f975 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs @@ -0,0 +1,75 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class OptionNamingPolicyTests +{ + [TestMethod] + [DataRow(new[] { "--option-name1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option-name1=value")] + [DataRow(new[] { "-OptionName1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -OptionName1=value")] + [DataRow(new[] { "--option-name1=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option-name1=value")] + [DataRow(new[] { "--option-name1=value" }, TestCommandLineStyle.Gnu, DisplayName = "[GNU] --option-name1=value")] + [DataRow(new[] { "-OptionName1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[Flexible] -OptionName1=value")] + public void Supported_KebabCase(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.OptionName1); + } + + [TestMethod] + [DataRow(new[] { "--OptionName2=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --OptionName2=value")] + [DataRow(new[] { "-OptionName2=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -OptionName2=value")] + [DataRow(new[] { "--OptionName2=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --OptionName2=value")] + [DataRow(new[] { "--OptionName2=value" }, TestCommandLineStyle.Gnu, DisplayName = "[GNU] --OptionName2=value")] + [DataRow(new[] { "-OptionName2=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -OptionName2=value")] + public void Supported_Ordinal(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.OptionName2); + } + + [TestMethod] + [DataRow(new[] { "--OptionName1=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --OptionName1=value")] + [DataRow(new[] { "--OptionName1=value" }, TestCommandLineStyle.Gnu, DisplayName = "[GNU] --OptionName1=value")] + [DataRow(new[] { "-option-name1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[Flexible] -option-name1=value")] + [DataRow(new[] { "--option-name2=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option-name2=value")] + [DataRow(new[] { "--option-name2=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option-name2=value")] + [DataRow(new[] { "--option-name2=value" }, TestCommandLineStyle.Gnu, DisplayName = "[GNU] --option-name2=value")] + [DataRow(new[] { "-option-name2=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option-name2=value")] + public void NotSupported(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); + } + + public record TestOptions + { + [Option("option-name1")] + public string? OptionName1 { get; set; } + + [Option("OptionName2")] + public string? OptionName2 { get; set; } + } +} From 75120a46c19323f7567333f98233c96a454244af Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 19:29:01 +0800 Subject: [PATCH 093/193] =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E5=86=99=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/OptionCaseSensitiveTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs new file mode 100644 index 00000000..cdde6c2e --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs @@ -0,0 +1,56 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class OptionCaseSensitiveTests +{ + [TestMethod] + [DataRow(new[] { "--Option-Name1=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --Option-Name1=value")] + [DataRow(new[] { "--OPTION-NAME1=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --OPTION-NAME1=value")] + [DataRow(new[] { "--Option-Name1=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --Option-Name1=value")] + [DataRow(new[] { "--OPTION-NAME1=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --OPTION-NAME1=value")] + public void CaseSensitive(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); + } + + [TestMethod] + [DataRow(new[] { "--Option-Name1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --Option-Name1=value")] + [DataRow(new[] { "--OPTION-NAME1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --OPTION-NAME1=value")] + [DataRow(new[] { "-optionName1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -optionName1=value")] + [DataRow(new[] { "-optionname1=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -optionname1=value")] + [DataRow(new[] { "-optionname1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -optionname1=value")] + [DataRow(new[] { "-OPTIONNAME1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -OPTIONNAME1=value")] + [DataRow(new[] { "-optionName1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -optionName1=value")] + public void CaseInsensitive(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.OptionName1); + } + + public record TestOptions + { + [Option("option-name1")] + public string? OptionName1 { get; set; } + + [Option("OptionName2")] + public string? OptionName2 { get; set; } + } +} From 8cd35f5def18cd6949a8f912e3896ca801adfc06 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 20:49:01 +0800 Subject: [PATCH 094/193] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/PositionalArgumentTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs new file mode 100644 index 00000000..9e456002 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs @@ -0,0 +1,38 @@ +using DotNetCampus.Cli.Compiler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class PositionalArgumentTests +{ + [TestMethod] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o option value")] + [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o option value")] + [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o option value")] + [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o option value")] + public void Supported(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.Value); + } + + public record TestOptions + { + [Option('o', "option")] + public string? Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } +} From 2d6b85830e67bc15b431afdf95fe272f7fddf7cb Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 20:57:44 +0800 Subject: [PATCH 095/193] =?UTF-8?q?PowerShell=20=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=90=8E=E7=BD=AE=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Parsers/CommandLineParser.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 8e4d12da..dd4e57aa 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -480,7 +480,9 @@ private bool ParseCommandRegion() ///
private bool ParseOptionAndPositionalArgumentRegion() { - var isPostPositionalArgument = string.Equals(_argument, "--", StringComparison.Ordinal); + // 只有使用双前缀的风格才支持后置位置参数区。 + var isPostPositionalArgument = _parser.OptionPrefix is CommandOptionPrefix.DoubleDash or CommandOptionPrefix.Any + && string.Equals(_argument, "--", StringComparison.Ordinal); if (isPostPositionalArgument) { Type = Cat.PositionalArgumentSeparator; From e4fd14ff57064441bd086c4197401cf178c2e6d4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 20:57:57 +0800 Subject: [PATCH 096/193] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/PositionalArgumentTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs index 9e456002..e1bfaf54 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs @@ -1,4 +1,6 @@ using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Utils.Parsers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DotNetCampus.Cli.Tests.ParsingStyles; @@ -15,6 +17,14 @@ public class PositionalArgumentTests [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o option value")] [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o option value")] [DataRow(new[] { "-o", "option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o option value")] + [DataRow(new[] { "value", "-o", "option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value -o option")] + [DataRow(new[] { "value", "-o", "option" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value -o option")] + [DataRow(new[] { "value", "-o", "option" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value -o option")] + [DataRow(new[] { "value", "-o", "option" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value -o option")] + [DataRow(new[] { "-o", "option", "--", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o option -- value")] + [DataRow(new[] { "-o", "option", "--", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o option -- value")] + [DataRow(new[] { "-o", "option", "--", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o option -- value")] + [DataRow(new[] { "test://value?option=option" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value?option=option")] public void Supported(string[] args, TestCommandLineStyle style) { // Arrange @@ -27,6 +37,20 @@ public void Supported(string[] args, TestCommandLineStyle style) Assert.AreEqual("value", options.Value); } + [TestMethod] + [DataRow(new[] { "-o", "option", "--", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o option -- value")] + public void DoesNotSupportPostPositionalArguments(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); + } + public record TestOptions { [Option('o', "option")] From 7962217afa30ec803a47aa06454879bb1cd0697d Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 21:18:31 +0800 Subject: [PATCH 097/193] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20scheme=20=E6=8B=BC?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/README.md | 2 +- docs/zh-hant/README.md | 2 +- .../Utils/CommandLineConverter.cs | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 276cf179..736d467d 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ The library supports multiple command line styles via `CommandLineStyle` (Flexib - Positional arguments via `ValueAttribute` (ranges allowed) - Required / nullable / immutable (`required` / `init`) property semantics - Command & subcommand handling (multi-word `[Command]` supported) -- Optional URL protocol parsing (`schema://...` form) +- Optional URL protocol parsing (`scheme://...` form) - High performance from source generators and interceptors ## Engage, Contribute and Provide Feedback diff --git a/docs/en/README.md b/docs/en/README.md index f45cebde..7c3dac4f 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -282,7 +282,7 @@ commandLine DotNetCampus.CommandLine can parse a URL protocol string: ```ini -// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 +// scheme://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 ``` The example near the top expressed as URL: diff --git a/docs/zh-hant/README.md b/docs/zh-hant/README.md index 4ef8051f..4b4ae117 100644 --- a/docs/zh-hant/README.md +++ b/docs/zh-hant/README.md @@ -282,7 +282,7 @@ commandLine 可解析 URL 協議字串: ```ini -// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 +// scheme://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 ``` 開頭示例命令列可寫成: diff --git a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs index 6c529888..bb8cf20d 100644 --- a/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs +++ b/src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs @@ -91,17 +91,17 @@ internal static (string? MatchedUrlScheme, IReadOnlyList? UrlNormalizedA /// /// 将 URL 转换为普通的命令行参数列表。
///
- /// URL 的 Scheme 部分。 + /// URL 的 Scheme 部分。 /// URL 字符串。 /// 普通的命令行参数列表。 - private static IReadOnlyList NormalizeUrlArguments(string schema, string argument) + private static IReadOnlyList NormalizeUrlArguments(string scheme, string argument) { - // schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 + // scheme://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 var span = argument.AsSpan(); - // 1. 跳过 schema:// - span = span[(schema.Length + 3)..]; + // 1. 跳过 scheme:// + span = span[(scheme.Length + 3)..]; // 2. 分成三个部分,分别解析。 var questionMarkIndex = span.IndexOf('?'); From 1e6daccf13193ad682160e6c4c3a9e9dc36027a4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 21:19:15 +0800 Subject: [PATCH 098/193] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=AF=A6=E7=BB=86=E6=95=B4=E7=90=86=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E9=A3=8E=E6=A0=BC=E5=8C=BA=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-hans/README.md | 93 ++++++++++++------- .../ParsingStyles/README.md | 72 ++++++++------ 2 files changed, 102 insertions(+), 63 deletions(-) diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index 45b3cf36..4e6e454a 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -89,35 +89,60 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); 默认情况下,这些风格的详细区别如下: -| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | -| ---------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | -| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | -| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | -| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | -| 选项值 `=` | -o=value | -o=value | -o=value | | | option=value | -| 选项值 `:` | -o:value | -o:value | | | | | -| 选项值 ` ` | -o value | -o value | -o value | -o value | -o value | | -| 布尔选项 | -o | -o | -o | -o | -o | option | -| 布尔选项 | -o=true | -o=true | | | -o:true | option=true | -| 布尔值 | true/false | true/false | true/false | true/false | true/false | true/false | -| 布尔值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | -| 布尔值 | on/off | on/off | on/off | on/off | on/off | on/off | -| 布尔值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | -| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | -| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | -| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | -| 多短布尔选项合并 | 不支持 | 不支持 | -abc | -abc | 不支持 | 不支持 | -| 单短选项多字符 | -ab | -ab | 不支持 | 不支持 | -ab | 不支持 | -| 短选项直接带值 | 不支持 | 不支持 | -o1.txt | 不支持 | 不支持 | 不支持 | -| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | -| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | -| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | -| 命名法 | -PascalCase | | | | -PascalCase | | -| 命名法 | -camelCase | | | | -camelCase | | -| 命名法 | /PascalCase | | | | /PascalCase | | -| 命名法 | /camelCase | | | | /camelCase | | +| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ----------------- | -------------- | -------------- | ----------------- | ---------- | ------------ | ----------------- | +| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | +| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | +| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | +| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 长选项 ` ` | --option value | --option value | -o value | -o value | -o value | | +| 长选项 `=` | --option=value | --option=value | --option=value | | -o=value | option=value | +| 长选项 `:` | --option:value | --option:value | | | -o:value | | +| 短选项 ` ` | -o value | -o value | -o value | -o value | -o value | | +| 短选项 `=` | -o=value | -o=value | | | -o=value | option=value | +| 短选项 `:` | -o:value | -o:value | | | -o:value | | +| 短选项 `null` | | | -ovalue | | | | +| 多字符短选项 | -abc value | -abc value | | | -abc value | | +| 长布尔选项 | --option | --option | --option | | -Option | option | +| 长布尔选项 ` ` | --option true | --option true | | | -Option true | | +| 长布尔选项 `=` | --option=true | --option=true | --option=true[^1] | | -Option=true | | +| 长布尔选项 `:` | --option:true | --option:true | | | -Option:true | | +| 短选项选项 | -o | -o | -o | -o | -o | | +| 短选项选项 ` ` | -o true | -o true | | | -o true | | +| 短选项选项 `=` | -o=true | -o=true | | | -o=true | option=true | +| 短选项选项 `:` | -o:true | -o:true | | | -o:true | | +| 短选项选项 `null` | | | -o1 | | | | +| 布尔/开关值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布尔/开关值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布尔/开关值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布尔/开关值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 短布尔选项合并 | | | -abc | -abc | | | +| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | +| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | | +| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | | +| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | +| 位置参数 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | +| 后置位置参数 `--` | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | + +[^1]: GNU 风格并不支持布尔选项显式带值,但因为这种情况并没有歧义,所以我们考虑额外支持它。 + +说明: + +1. 除 PowerShell 风格外,其他风格均支持 `--` 作为后置位置参数的标记,之后的所有参数均视为位置参数;另外,URL 风格写不出来后置位置参数。 +1. 在 `--` 之前,选项和位置参数是可以混合使用的,规则如下。 + +选项会优先取出紧跟着的值,但凡能放入该选项的,均会放入该选项,一旦放不下了,后面如果还有值,就会算作位置参数。 + +例如,`--option` 是个布尔选项时,`--option true text` 或 `--option 1 text` 后面的 `true` 和 `1` 会被 `--option` 选项取走,再后面的 `text` 则就位置参数。 +再例如,`--option` 是个布尔选项时,`--option text` 由于 `text` 不是布尔值,所以 `text` 直接就是位置参数。 +再例如,如果风格支持空格分隔集合(见上表),那么当 `--option a b c` 是个集合选项时,`a` `b` `c` 都会被取走,直到遇到下一个选项或 `--`。GNU 不支持空格分隔集合。 ## 命名法 @@ -290,7 +315,7 @@ commandLine DotNetCampus.CommandLine 支持解析 URL 协议字符串,格式如下: ```ini -// schema://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 +// scheme://command/subcommand/positional-argument1/positional-argument2?option1=value1&option2=value2 ``` 本文开头示例中的那个命令行,使用 URL 传入的话将是下面这样: @@ -349,8 +374,8 @@ public class BenchmarkOptions41 解析空白命令行参数: -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|------------------------------ |-------------:|-----------:|-----------:|-------:|----------:| +| Method | Mean | Error | StdDev | Gen0 | Allocated | +| ----------------------------- | -----------: | ---------: | ---------: | -----: | --------: | | 'parse [] -v=4.1 -p=flexible' | 27.25 ns | 0.485 ns | 0.454 ns | 0.0143 | 240 B | | 'parse [] -v=4.1 -p=dotnet' | 27.35 ns | 0.471 ns | 0.440 ns | 0.0143 | 240 B | | 'parse [] -v=4.0 -p=flexible' | 97.16 ns | 0.708 ns | 0.628 ns | 0.0134 | 224 B | @@ -364,8 +389,8 @@ public class BenchmarkOptions41 test DotNetCampus.CommandLine.Performance.dll DotNetCampus.CommandLine.Sample.dll DotNetCampus.CommandLine.Test.dll -c 20 --test-name BenchmarkTest --detail-level High --debug ``` -| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated | -|--------------------------------- |-------------- |-------------- |------------:|----------:|----------:|-------:|----------:| +| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated | +| -------------------------------- | ------------- | ------------- | ----------: | --------: | --------: | -----: | --------: | | 'parse [GNU] -v=4.1 -p=flexible' | .NET 10.0 | .NET 10.0 | 355.9 ns | 4.89 ns | 4.58 ns | 0.0548 | 920 B | | 'parse [GNU] -v=4.1 -p=gnu' | .NET 10.0 | .NET 10.0 | 339.7 ns | 6.81 ns | 7.57 ns | 0.0548 | 920 B | | 'parse [GNU] -v=4.0 -p=flexible' | .NET 10.0 | .NET 10.0 | 945.9 ns | 14.87 ns | 13.19 ns | 0.1583 | 2656 B | diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md index a1294fa5..1bca9acb 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md @@ -17,35 +17,49 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); 默认情况下,这些风格的详细区别如下: -| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | -| ---------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | -| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | -| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | -| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | -| 选项值 `=` | -o=value | -o=value | -o=value | | | option=value | -| 选项值 `:` | -o:value | -o:value | | | | | -| 选项值 ` ` | -o value | -o value | -o value | -o value | -o value | | -| 布尔选项 | -o | -o | -o | -o | -o | option | -| 布尔选项 | -o=true | -o=true | | | -o:true | option=true | -| 布尔值 | true/false | true/false | true/false | true/false | true/false | true/false | -| 布尔值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | -| 布尔值 | on/off | on/off | on/off | on/off | on/off | on/off | -| 布尔值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | -| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | -| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | -| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | -| 多短布尔选项合并 | 不支持 | 不支持 | -abc | -abc | 不支持 | 不支持 | -| 单短选项多字符 | -ab | -ab | 不支持 | 不支持 | -ab | 不支持 | -| 短选项直接带值 | 不支持 | 不支持 | -o1.txt | 不支持 | 不支持 | 不支持 | -| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | -| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | -| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | -| 命名法 | -PascalCase | | | | -PascalCase | | -| 命名法 | -camelCase | | | | -camelCase | | -| 命名法 | /PascalCase | | | | /PascalCase | | -| 命名法 | /camelCase | | | | /camelCase | | +| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ----------------- | -------------- | -------------- | ----------------- | ---------- | ------------ | ----------------- | +| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | +| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | +| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | +| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 长选项 ` ` | --option value | --option value | -o value | -o value | -o value | | +| 长选项 `=` | --option=value | --option=value | --option=value | | -o=value | option=value | +| 长选项 `:` | --option:value | --option:value | | | -o:value | | +| 短选项 ` ` | -o value | -o value | -o value | -o value | -o value | | +| 短选项 `=` | -o=value | -o=value | | | -o=value | option=value | +| 短选项 `:` | -o:value | -o:value | | | -o:value | | +| 短选项 `null` | | | -ovalue | | | | +| 多字符短选项 | -abc value | -abc value | | | -abc value | | +| 长布尔选项 | --option | --option | --option | | -Option | option | +| 长布尔选项 ` ` | --option true | --option true | | | -Option true | | +| 长布尔选项 `=` | --option=true | --option=true | --option=true[^1] | | -Option=true | | +| 长布尔选项 `:` | --option:true | --option:true | | | -Option:true | | +| 短选项选项 | -o | -o | -o | -o | -o | | +| 短选项选项 ` ` | -o true | -o true | | | -o true | | +| 短选项选项 `=` | -o=true | -o=true | | | -o=true | option=true | +| 短选项选项 `:` | -o:true | -o:true | | | -o:true | | +| 短选项选项 `null` | | | -o1 | | | | +| 布尔/开关值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布尔/开关值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布尔/开关值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布尔/开关值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 短布尔选项合并 | | | -abc | -abc | | | +| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | +| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | | +| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | | +| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | +| 位置参数 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | +| 后置位置参数 `--` | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | + +[^1]: GNU 风格并不支持布尔选项显式带值,但因为这种情况并没有歧义,所以我们考虑额外支持它。 ## 必需选项与默认值 From ec75c4bbc1a925b4530dfbe5c83f13d3733bb273 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 21:25:08 +0800 Subject: [PATCH 099/193] =?UTF-8?q?AI=20=E5=B0=86=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8C=E6=AD=A5=E5=88=B0=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/README.md | 83 +++++++++++++++++++++++++++--------------- docs/zh-hans/README.md | 4 +- docs/zh-hant/README.md | 83 +++++++++++++++++++++++++++--------------- 3 files changed, 110 insertions(+), 60 deletions(-) diff --git a/docs/en/README.md b/docs/en/README.md index 7c3dac4f..345d9e43 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -89,35 +89,60 @@ Supported styles include: By default, their detailed differences are: -| Style | Flexible | DotNet | Gnu | Posix | PowerShell | URL | -| ----------------------------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ----------------- | -| Case | Insensitive | Sensitive | Sensitive | Sensitive | Insensitive | Insensitive | -| Long options | Supported | Supported | Supported | Not supported | Supported | Supported | -| Short options | Supported | Supported | Supported | Supported | Supported | Not supported | -| Option value `=` | -o=value | -o=value | -o=value | | | option=value | -| Option value `:` | -o:value | -o:value | | | | | -| Option value (space) | -o value | -o value | -o value | -o value | -o value | | -| Boolean option (implicit true) | -o | -o | -o | -o | -o | option | -| Boolean option (with value) | -o=true | -o=true | | | -o:true | option=true | -| Boolean values | true/false | true/false | true/false | true/false | true/false | true/false | -| Boolean values | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | -| Boolean values | on/off | on/off | on/off | on/off | on/off | on/off | -| Boolean values | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | -| Collection option | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | -| Collection option `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -| Collection option `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -| Collection option (space separated) | -o A B C | -o A B C | | | -o A B C | | -| Dictionary option | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | -| Combined short booleans | Not supported | Not supported | -abc | -abc | Not supported | Not supported | -| Single short option multi chars | -ab | -ab | Not supported | Not supported | -ab | Not supported | -| Short option directly with value | Not supported | Not supported | -o1.txt | Not supported | Not supported | Not supported | -| Long option prefixes | `--` `-` `/` | `--` | `--` | (None) | `-` `/` | | -| Short option prefixes | `-` `/` | `-` | `-` | `-` | `-` `/` | | -| Naming | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | -| Naming | -PascalCase | | | | -PascalCase | | -| Naming | -camelCase | | | | -camelCase | | -| Naming | /PascalCase | | | | /PascalCase | | -| Naming | /camelCase | | | | /camelCase | | +| Style | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| --------------------- | -------------- | -------------- | ----------------- | ------------- | ------------- | ----------------- | +| Positional args | Supported | Supported | Supported | Supported | Supported | Supported | +| Trailing args `--` | Supported | Supported | Supported | Supported | Not supported | Not supported | +| Case | Insensitive | Sensitive | Sensitive | Sensitive | Insensitive | Insensitive | +| Long options | Supported | Supported | Supported | Not supported | Supported | Supported | +| Short options | Supported | Supported | Supported | Supported | Supported | Not supported | +| Long option prefixes | `--` `-` `/` | `--` | `--` | (None) | `-` `/` | | +| Short option prefixes | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| Long option (space) | --option value | --option value | -o value | -o value | -o value | | +| Long option `=` | --option=value | --option=value | --option=value | | -o=value | option=value | +| Long option `:` | --option:value | --option:value | | | -o:value | | +| Short option (space) | -o value | -o value | -o value | -o value | -o value | | +| Short option `=` | -o=value | -o=value | | | -o=value | option=value | +| Short option `:` | -o:value | -o:value | | | -o:value | | +| Short option inline | | | -ovalue | | | | +| Multi-char short opt | -abc value | -abc value | | | -abc value | | +| Long boolean option | --option | --option | --option | | -Option | option | +| Long boolean ` ` | --option true | --option true | | | -Option true | | +| Long boolean `=` | --option=true | --option=true | --option=true[^1] | | -Option=true | | +| Long boolean `:` | --option:true | --option:true | | | -Option:true | | +| Short boolean option | -o | -o | -o | -o | -o | | +| Short boolean ` ` | -o true | -o true | | | -o true | | +| Short boolean `=` | -o=true | -o=true | | | -o=true | option=true | +| Short boolean `:` | -o:true | -o:true | | | -o:true | | +| Short boolean inline | | | -o1 | | | | +| Boolean values | true/false | true/false | true/false | true/false | true/false | true/false | +| Boolean values | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| Boolean values | on/off | on/off | on/off | on/off | on/off | on/off | +| Boolean values | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| Combined short bools | | | -abc | -abc | | | +| Collection option | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| Collection (space) | -o A B C | -o A B C | | | -o A B C | | +| Collection `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | | +| Collection `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | | +| Dictionary option | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| Naming | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| Naming | -PascalCase | | | | -PascalCase | | +| Naming | -camelCase | | | | -camelCase | | +| Naming | /PascalCase | | | | /PascalCase | | +| Naming | /camelCase | | | | /camelCase | | + +[^1]: GNU style does not officially support supplying an explicit value to a boolean option, but since the syntax is unambiguous we additionally allow it. + +Notes: + +1. Except for PowerShell style, all other styles support using `--` to mark the start of trailing positional arguments; everything after is treated as a positional argument. URL style cannot express trailing positionals. +2. Before `--`, options and positional arguments may be interleaved. The rule: an option greedily consumes following tokens as long as they can be accepted by that option; once it can no longer take a token, the remaining tokens (until the next option or `--`) are treated as positional arguments. + +An option takes the immediate values greedily: + +For example, if `--option` is a boolean option, then in `--option true text` or `--option 1 text`, the `true` or `1` is consumed by `--option`, and `text` becomes a positional argument. +Another example: if `--option` is a boolean option, `--option text` leaves `text` as a positional argument because it is not a boolean value. +Another example: if a style supports space separated collections (see table), then when `--option a b c` is a collection option, `a` `b` `c` are consumed until the next option or `--`. GNU does not support space separated collections. ## Naming diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index 4e6e454a..4b73b518 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -91,6 +91,8 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); | 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | | ----------------- | -------------- | -------------- | ----------------- | ---------- | ------------ | ----------------- | +| 位置参数 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | +| 后置位置参数 `--` | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | | 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | | 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | | 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | @@ -128,8 +130,6 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); | 命名法 | -camelCase | | | | -camelCase | | | 命名法 | /PascalCase | | | | /PascalCase | | | 命名法 | /camelCase | | | | /camelCase | | -| 位置参数 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | -| 后置位置参数 `--` | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | [^1]: GNU 风格并不支持布尔选项显式带值,但因为这种情况并没有歧义,所以我们考虑额外支持它。 diff --git a/docs/zh-hant/README.md b/docs/zh-hant/README.md index 4b4ae117..2c6fb885 100644 --- a/docs/zh-hant/README.md +++ b/docs/zh-hant/README.md @@ -89,35 +89,60 @@ var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); 預設情況下,這些風格的詳細差異如下: -| 風格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | -| -------------- | ------------ | ------------ | ------------ | ---------- | ----------- | ----------------- | -| 大小寫 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | -| 長選項 | 支援 | 支援 | 支援 | 不支援 | 支援 | 支援 | -| 短選項 | 支援 | 支援 | 支援 | 支援 | 支援 | 不支援 | -| 選項值 `=` | -o=value | -o=value | -o=value | | | option=value | -| 選項值 `:` | -o:value | -o:value | | | | | -| 選項值 空白 | -o value | -o value | -o value | -o value | -o value | | -| 布林選項 | -o | -o | -o | -o | -o | option | -| 布林選項帶值 | -o=true | -o=true | | | -o:true | option=true | -| 布林值 | true/false | true/false | true/false | true/false | true/false | true/false | -| 布林值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | -| 布林值 | on/off | on/off | on/off | on/off | on/off | on/off | -| 布林值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | -| 集合選項 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | -| 集合選項 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -| 集合選項 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -| 集合選項 空白 | -o A B C | -o A B C | | | -o A B C | | -| 字典選項 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | -| 多短布林合併 | 不支援 | 不支援 | -abc | -abc | 不支援 | 不支援 | -| 單短選項多字元 | -ab | -ab | 不支援 | 不支援 | -ab | 不支援 | -| 短選項直接帶值 | 不支援 | 不支援 | -o1.txt | 不支援 | 不支援 | 不支援 | -| 長選項前綴 | `--` `-` `/` | `--` | `--` | 不支援 | `-` `/` | | -| 短選項前綴 | `-` `/` | `-` | `-` | `-` | `-` `/` | | -| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | -| 命名法 | -PascalCase | | | | -PascalCase | | -| 命名法 | -camelCase | | | | -camelCase | | -| 命名法 | /PascalCase | | | | /PascalCase | | -| 命名法 | /camelCase | | | | /camelCase | | +| 風格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | +| ----------------- | -------------- | -------------- | ----------------- | ---------- | ------------ | ----------------- | +| 位置參數 | 支援 | 支援 | 支援 | 支援 | 支援 | 支援 | +| 後置位置參數 `--` | 支援 | 支援 | 支援 | 支援 | 不支援 | 不支援 | +| 大小寫 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | +| 長選項 | 支援 | 支援 | 支援 | 不支援 | 支援 | 支援 | +| 短選項 | 支援 | 支援 | 支援 | 支援 | 支援 | 不支援 | +| 長選項前綴 | `--` `-` `/` | `--` | `--` | 不支援 | `-` `/` | | +| 短選項前綴 | `-` `/` | `-` | `-` | `-` | `-` `/` | | +| 長選項 ` ` | --option value | --option value | -o value | -o value | -o value | | +| 長選項 `=` | --option=value | --option=value | --option=value | | -o=value | option=value | +| 長選項 `:` | --option:value | --option:value | | | -o:value | | +| 短選項 ` ` | -o value | -o value | -o value | -o value | -o value | | +| 短選項 `=` | -o=value | -o=value | | | -o=value | option=value | +| 短選項 `:` | -o:value | -o:value | | | -o:value | | +| 短選項 `null` | | | -ovalue | | | | +| 多字元短選項 | -abc value | -abc value | | | -abc value | | +| 長布林選項 | --option | --option | --option | | -Option | option | +| 長布林選項 ` ` | --option true | --option true | | | -Option true | | +| 長布林選項 `=` | --option=true | --option=true | --option=true[^1] | | -Option=true | | +| 長布林選項 `:` | --option:true | --option:true | | | -Option:true | | +| 短布林選項 | -o | -o | -o | -o | -o | | +| 短布林選項 ` ` | -o true | -o true | | | -o true | | +| 短布林選項 `=` | -o=true | -o=true | | | -o=true | option=true | +| 短布林選項 `:` | -o:true | -o:true | | | -o:true | | +| 短布林選項 `null` | | | -o1 | | | | +| 布林/開關值 | true/false | true/false | true/false | true/false | true/false | true/false | +| 布林/開關值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | +| 布林/開關值 | on/off | on/off | on/off | on/off | on/off | on/off | +| 布林/開關值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | +| 多短布林合併 | | | -abc | -abc | | | +| 集合選項 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | +| 集合選項 ` ` | -o A B C | -o A B C | | | -o A B C | | +| 集合選項 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | | +| 集合選項 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | | +| 字典選項 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | +| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | +| 命名法 | -PascalCase | | | | -PascalCase | | +| 命名法 | -camelCase | | | | -camelCase | | +| 命名法 | /PascalCase | | | | /PascalCase | | +| 命名法 | /camelCase | | | | /camelCase | | + +[^1]: GNU 風格並不支援布林選項顯式帶值,但因為這種情況沒有歧義,所以我們額外支援它。 + +說明: + +1. 除 PowerShell 風格外,其他風格都支援使用 `--` 作為後置位置參數標記,其後所有參數皆視為位置參數;另外,URL 風格無法表達後置位置參數。 +2. 在 `--` 之前,選項與位置參數可以交錯出現,規則如下。 + +選項會優先取得緊跟的值;凡是能放進該選項的值都會被取走。一旦放不下,後面若還有值,就視為位置參數。 + +例如,`--option` 是布林選項時,`--option true text` 或 `--option 1 text` 中的 `true` 與 `1` 會被 `--option` 取走,之後的 `text` 為位置參數。 +再例如,`--option` 是布林選項時,`--option text` 因為 `text` 不是布林值,所以 `text` 直接視為位置參數。 +再例如,若風格支援空白分隔集合(見上表),則當 `--option a b c` 是集合選項時,`a` `b` `c` 都會被取走,直到遇到下一個選項或 `--`。GNU 不支援空白分隔集合。 ## 命名法 From cab1bb0fce26e41c574a5e45d53e7848c3f968df Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 21:46:59 +0800 Subject: [PATCH 100/193] =?UTF-8?q?=E5=8C=B9=E9=85=8D=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/PositionalArgumentTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs index e1bfaf54..ee9bfe21 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs @@ -1,3 +1,5 @@ +using System.Collections; +using System.Collections.Generic; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Exceptions; using DotNetCampus.Cli.Utils.Parsers; @@ -37,6 +39,63 @@ public void Supported(string[] args, TestCommandLineStyle style) Assert.AreEqual("value", options.Value); } + [TestMethod] + [DataRow(new[] { "-o", "true", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o true value")] + [DataRow(new[] { "-o", "true", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o true value")] + [DataRow(new[] { "-o", "true", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o true value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o value")] + [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o value")] + public void Supported_Boolean(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsTrue(options.Option); + Assert.AreEqual("value", options.Value); + } + + [TestMethod] + [DataRow(new[] { "value", "-o", "a", "b" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value -o a b")] + [DataRow(new[] { "value", "-o", "a", "b" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value -o a b")] + [DataRow(new[] { "value", "-o", "a", "b" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value -o a b")] + [DataRow(new[] { "-o", "a", "b", "--", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a b -- value")] + [DataRow(new[] { "-o", "a", "b", "--", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a b -- value")] + public void Supported_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + CollectionAssert.AreEqual(new[] { "a", "b" }, (ICollection)options.Option!); + Assert.AreEqual("value", options.Value); + } + + [TestMethod] + [DataRow(new[] { "-o", "a", "b", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o a b value")] + [DataRow(new[] { "-o", "a", "b", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o a b value")] + [DataRow(new[] { "-o", "a", "b", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o a b value")] + public void NotSupported_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + CollectionAssert.AreEqual(new[] { "a", "b", "value" }, (ICollection)options.Option!); + Assert.IsNull(options.Value); + } + [TestMethod] [DataRow(new[] { "-o", "option", "--", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o option -- value")] public void DoesNotSupportPostPositionalArguments(string[] args, TestCommandLineStyle style) @@ -51,6 +110,41 @@ public void DoesNotSupportPostPositionalArguments(string[] args, TestCommandLine Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); } + // [TestMethod] + public void MatchPositionalArgumentRange(string[] args, TestCommandLineStyle style) + { + } + + [TestMethod] + [DataRow(new[] { "-o", "true", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o true value")] + public void DoesNotMatchPositionalArgumentRange_Boolean(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + + [TestMethod] + [DataRow(new[] { "value", "-o", "a", "b" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value -o a b")] + [DataRow(new[] { "-o", "a", "b", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a b value")] + [DataRow(new[] { "-o", "a", "b", "--", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o a b -- value")] + public void DoesNotMatchPositionalArgumentRange_Collection(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine.As()); + + // Assert + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + public record TestOptions { [Option('o', "option")] @@ -59,4 +153,34 @@ public record TestOptions [Value(0)] public string? Value { get; set; } } + + public record BooleanTestOptions + { + [Option('o', "option")] + public bool? Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record CollectionTestOptions + { + [Option('o', "option")] + public IReadOnlyList? Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record MultiplePositionArgumentsOptions + { + [Value(0, 2)] + public IReadOnlyList? Value0 { get; set; } + + [Value(1)] + public string? Value1 { get; set; } + + [Value(2, int.MaxValue)] + public IReadOnlyList? Value2 { get; set; } + } } From c0f49ecb9fda660ffaedc84ebfa9f47e241c2e75 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 20 Sep 2025 22:22:13 +0800 Subject: [PATCH 101/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=AD=90=E5=91=BD=E4=BB=A4=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandMatching/MatchCommandTests.cs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs new file mode 100644 index 00000000..33522486 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs @@ -0,0 +1,131 @@ +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using DotNetCampus.Cli.Tests.ParsingStyles; +using DotNetCampus.Cli.Utils.Parsers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.CommandMatching; + +[TestClass] +public class MatchCommandTests +{ + [TestMethod] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] No command")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] No command")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] No command")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] No command")] + [DataRow(new[] { "test://" }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Url, DisplayName = "[Url] No command")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] foo")] + [DataRow(new[] { "test://foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Url, DisplayName = "[Url] test://foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] foo")] + [DataRow(new[] { "fooo" }, nameof(DefaultOptions), "fooo", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] fooo")] + [DataRow(new[] { "fooo" }, nameof(DefaultOptions), "fooo", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] fooo")] + [DataRow(new[] { "fooo" }, nameof(DefaultOptions), "fooo", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] fooo")] + [DataRow(new[] { "fooo" }, nameof(DefaultOptions), "fooo", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] fooo")] + [DataRow(new[] { "test://fooo" }, nameof(DefaultOptions), "fooo", TestCommandLineStyle.Url, DisplayName = "[Url] test://fooo")] + [DataRow(new[] { "bar", "baz" }, nameof(BarBazOptions), "BarBaz", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] bar baz")] + [DataRow(new[] { "bar", "baz" }, nameof(BarBazOptions), "BarBaz", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] bar baz")] + [DataRow(new[] { "bar", "baz" }, nameof(BarBazOptions), "BarBaz", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] bar baz")] + [DataRow(new[] { "bar", "baz" }, nameof(BarBazOptions), "BarBaz", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] bar baz")] + [DataRow(new[] { "test://bar/baz" }, nameof(BarBazOptions), "BarBaz", TestCommandLineStyle.Url, DisplayName = "[Url] test://bar/baz")] + [DataRow(new[] { "bar", "bazz" }, nameof(BarOptions), "bazz", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] bar bazz")] + [DataRow(new[] { "bar", "bazz" }, nameof(BarOptions), "bazz", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] bar bazz")] + [DataRow(new[] { "bar", "bazz" }, nameof(BarOptions), "bazz", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] bar bazz")] + [DataRow(new[] { "bar", "bazz" }, nameof(BarOptions), "bazz", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] bar bazz")] + [DataRow(new[] { "test://bar/bazz" }, nameof(BarOptions), "bazz", TestCommandLineStyle.Url, DisplayName = "[Url] test://bar/bazz")] + [DataRow(new[] { "another", "sub-command" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.Flexible, + DisplayName = "[Flexible] another sub-command")] + [DataRow(new[] { "another", "sub-command" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.DotNet, + DisplayName = "[DotNet] another sub-command")] + [DataRow(new[] { "another", "sub-command" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.Gnu, + DisplayName = "[Gnu] another sub-command")] + [DataRow(new[] { "Another", "SubCommand" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.PowerShell, + DisplayName = "[PowerShell] Another SubCommand")] + [DataRow(new[] { "another", "subCommand" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.PowerShell, + DisplayName = "[PowerShell] another subCommand")] + [DataRow(new[] { "test://another/sub-command" }, nameof(SubCommandOptions), "AnotherSubCommand", TestCommandLineStyle.Url, + DisplayName = "[Url] test://another/sub-command")] + public void MatchCommand(string[] args, string expectedCommand, string expectedValue, TestCommandLineStyle style) + { + // Arrange + (string? TypeName, string? Value) matched = default; + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + commandLine + .AddHandler(o => matched = (o.GetType().Name, o.Value)) + .AddHandler(o => matched = (o.GetType().Name, o.Value)) + .AddHandler(o => matched = (o.GetType().Name, o.Value)) + .AddHandler(o => matched = (o.GetType().Name, o.Value)) + .AddHandler(o => matched = (o.GetType().Name, o.Value)) + .Run(); + + // Assert + Assert.AreEqual(expectedCommand, matched.TypeName); + Assert.AreEqual(expectedValue, matched.Value); + } + + [TestMethod] + [DataRow(new[] { "another", "sub-command" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] another sub-command")] + public void MatchCommand_PositionalArgumentNotMatch(string[] args, TestCommandLineStyle style) + { + // Arrange + var matched = ""; + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var exception = Assert.Throws(() => commandLine + .AddHandler(o => matched = o.Value) + .AddHandler(o => matched = o.Value) + .AddHandler(o => matched = o.Value) + .AddHandler(o => matched = o.Value) + .Run()); + + // Assert + Assert.IsEmpty(matched); + Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); + } + + public record DefaultOptions + { + [Value(0)] + public string? Value { get; set; } = "Default"; + } + + [Command("foo")] + public record FooOptions + { + [Value(0)] + public string? Value { get; set; } = "Foo"; + } + + [Command("bar")] + public record BarOptions + { + [Value(0)] + public string? Value { get; set; } = "Bar"; + } + + [Command("bar baz")] + public record BarBazOptions + { + [Value(0)] + public string? Value { get; set; } = "BarBaz"; + } + + [Command("bar qux")] + public record BarQuxOptions + { + [Value(0)] + public string? Value { get; set; } = "BarQux"; + } + + [Command("another sub-command")] + public record SubCommandOptions + { + [Value(0)] + public string? Value { get; set; } = "AnotherSubCommand"; + } +} From 1e317ed72bd6be9ed61f293a75f427d870500f1b Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 08:03:50 +0800 Subject: [PATCH 102/193] =?UTF-8?q?Flexible=20=E6=B5=8B=E8=AF=95=E6=9B=B4?= =?UTF-8?q?=E5=A4=9A=E7=9A=84=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/OptionBooleanValueTests.cs | 20 +++++++++++++++++ .../OptionDictionaryValueTests.cs | 2 -- .../OptionValueSeparatorTests.cs | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs index 6791d5f5..f5f70fc2 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs @@ -11,6 +11,10 @@ public class OptionBooleanValueTests [TestMethod] // option [DataRow(new[] { "--option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option")] + [DataRow(new[] { "-Option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option")] + [DataRow(new[] { "-option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option")] + [DataRow(new[] { "/Option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option")] + [DataRow(new[] { "/option" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option")] [DataRow(new[] { "--option" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option")] [DataRow(new[] { "--option" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option")] [DataRow(new[] { "-Option" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option")] @@ -19,12 +23,17 @@ public class OptionBooleanValueTests [DataRow(new[] { "/option" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option")] // o [DataRow(new[] { "-o" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o")] + [DataRow(new[] { "/o" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o")] [DataRow(new[] { "-o" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o")] [DataRow(new[] { "-o" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o")] [DataRow(new[] { "-o" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o")] [DataRow(new[] { "/o" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o")] // option=true [DataRow(new[] { "--option=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=true")] + [DataRow(new[] { "-Option=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option=true")] + [DataRow(new[] { "-option=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option=true")] + [DataRow(new[] { "/Option=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option=true")] + [DataRow(new[] { "/option=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option=true")] [DataRow(new[] { "--option=true" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=true")] [DataRow(new[] { "--option=true" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=true")] [DataRow(new[] { "-Option=true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=true")] @@ -33,11 +42,16 @@ public class OptionBooleanValueTests [DataRow(new[] { "/option=true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option=true")] // o=true [DataRow(new[] { "-o=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=true")] + [DataRow(new[] { "/o=true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o=true")] [DataRow(new[] { "-o=true" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=true")] [DataRow(new[] { "-o=true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=true")] [DataRow(new[] { "/o=true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o=true")] // option true [DataRow(new[] { "--option", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option true")] + [DataRow(new[] { "-Option", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option true")] + [DataRow(new[] { "-option", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option true")] + [DataRow(new[] { "/Option", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option true")] + [DataRow(new[] { "/option", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option true")] [DataRow(new[] { "--option", "true" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option true")] [DataRow(new[] { "-Option", "true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option true")] [DataRow(new[] { "-option", "true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option true")] @@ -45,6 +59,7 @@ public class OptionBooleanValueTests [DataRow(new[] { "/option", "true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option true")] // o true [DataRow(new[] { "-o", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o true")] + [DataRow(new[] { "/o", "true" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o true")] [DataRow(new[] { "-o", "true" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o true")] [DataRow(new[] { "-o", "true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o true")] [DataRow(new[] { "/o", "true" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o true")] @@ -138,6 +153,7 @@ public void OptionCombinationMustAllBoolean(string[] args, TestCommandLineStyle [TestMethod] [DataRow(new[] { "-ab" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab")] + [DataRow(new[] { "/ab" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab")] [DataRow(new[] { "-ab" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab")] [DataRow(new[] { "-ab" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab")] [DataRow(new[] { "/ab" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab")] @@ -155,8 +171,10 @@ public void DoesNotSupportBooleanOptionCombination(string[] args, TestCommandLin [TestMethod] [DataRow(new[] { "-ab" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab")] + [DataRow(new[] { "/ab" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab")] [DataRow(new[] { "-ab" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab")] [DataRow(new[] { "-ab" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab")] + [DataRow(new[] { "/ab" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab")] public void SupportMultiCharShortOptions(string[] args, TestCommandLineStyle style) { // Arrange @@ -172,8 +190,10 @@ public void SupportMultiCharShortOptions(string[] args, TestCommandLineStyle sty [TestMethod] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "/ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "/ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab value")] public void MultiCharShortOptionsDoesNotSupportValue(string[] args, TestCommandLineStyle style) { // Arrange diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs index c55a75e1..7121b5cd 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionDictionaryValueTests.cs @@ -1,8 +1,6 @@ using System.Collections; using System.Collections.Generic; using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Parsers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DotNetCampus.Cli.Tests.ParsingStyles; diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs index e3bfd4a8..61d66fef 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -11,6 +11,10 @@ public class OptionValueSeparatorTests [TestMethod] // option value [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option value")] + [DataRow(new[] { "-Option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option value")] + [DataRow(new[] { "-option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option value")] + [DataRow(new[] { "/Option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option value")] + [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option value")] [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option value")] [DataRow(new[] { "--option", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option value")] [DataRow(new[] { "-Option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option value")] @@ -19,12 +23,17 @@ public class OptionValueSeparatorTests [DataRow(new[] { "/option", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option value")] // o value [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o value")] + [DataRow(new[] { "/o", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o value")] [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value")] [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o value")] [DataRow(new[] { "-o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o value")] [DataRow(new[] { "/o", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o value")] // option=value [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option=value")] + [DataRow(new[] { "-Option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option=value")] + [DataRow(new[] { "-option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option=value")] + [DataRow(new[] { "/Option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option=value")] + [DataRow(new[] { "/option=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option=value")] [DataRow(new[] { "--option=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option=value")] [DataRow(new[] { "--option=value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] --option=value")] [DataRow(new[] { "-Option=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option=value")] @@ -34,11 +43,16 @@ public class OptionValueSeparatorTests [DataRow(new[] { "test://?option=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] option=value")] // o=value [DataRow(new[] { "-o=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o=value")] + [DataRow(new[] { "/o=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o=value")] [DataRow(new[] { "-o=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o=value")] [DataRow(new[] { "-o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o=value")] [DataRow(new[] { "/o=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o=value")] // option:value [DataRow(new[] { "--option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] --option:value")] + [DataRow(new[] { "-Option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -Option:value")] + [DataRow(new[] { "-option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -option:value")] + [DataRow(new[] { "/Option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /Option:value")] + [DataRow(new[] { "/option:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /option:value")] [DataRow(new[] { "--option:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] --option:value")] [DataRow(new[] { "-Option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -Option:value")] [DataRow(new[] { "-option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -option:value")] @@ -46,6 +60,7 @@ public class OptionValueSeparatorTests [DataRow(new[] { "/option:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /option:value")] // o:value [DataRow(new[] { "-o:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -o:value")] + [DataRow(new[] { "/o:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /o:value")] [DataRow(new[] { "-o:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o:value")] [DataRow(new[] { "-o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -o:value")] [DataRow(new[] { "/o:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /o:value")] @@ -98,6 +113,7 @@ public void GnuDoesNotSupportShortOptionSeparator(string[] args, string expected [TestMethod] [DataRow(new[] { "-ovalue" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ovalue")] + [DataRow(new[] { "/ovalue" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ovalue")] [DataRow(new[] { "-ovalue" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ovalue")] [DataRow(new[] { "-ovalue" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ovalue")] [DataRow(new[] { "/ovalue" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ovalue")] @@ -130,14 +146,20 @@ public void UrlStyleDoesNotSupportShortOption(string[] args, TestCommandLineStyl [TestMethod] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "/ab", "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] [DataRow(new[] { "-ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "/ab", "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab value")] [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "/ab=value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab value")] [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] [DataRow(new[] { "-ab=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "/ab=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab value")] [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] -ab value")] + [DataRow(new[] { "/ab:value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] /ab value")] [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -ab value")] [DataRow(new[] { "-ab:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -ab value")] + [DataRow(new[] { "/ab:value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] /ab value")] public void SupportMultiCharShortOptions(string[] args, TestCommandLineStyle style) { // Arrange From e7127684251fad80560e2587ad59ddfab1570a08 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 09:00:22 +0800 Subject: [PATCH 103/193] =?UTF-8?q?=E6=B5=8B=E6=9B=B4=E5=A4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandMatching/MatchCommandTests.cs | 11 ++++++----- .../ParsingStyles/OptionCaseSensitiveTests.cs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs index 33522486..914624f7 100644 --- a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs @@ -10,11 +10,12 @@ namespace DotNetCampus.Cli.Tests.CommandMatching; public class MatchCommandTests { [TestMethod] - [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] No command")] - [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] No command")] - [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] No command")] - [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] No command")] - [DataRow(new[] { "test://" }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Url, DisplayName = "[Url] No command")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, nameof(DefaultOptions), "Default", TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, nameof(DefaultOptions), "Default", TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "unknown://" }, nameof(DefaultOptions), "unknown://", TestCommandLineStyle.Url, DisplayName = "[Url] unknown://")] [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] foo")] [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] foo")] [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] foo")] diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs index cdde6c2e..eeaa4cc3 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs @@ -33,6 +33,8 @@ public void CaseSensitive(string[] args, TestCommandLineStyle style) [DataRow(new[] { "-optionname1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -optionname1=value")] [DataRow(new[] { "-OPTIONNAME1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -OPTIONNAME1=value")] [DataRow(new[] { "-optionName1=value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] -optionName1=value")] + [DataRow(new[] { "test://?Option-Name1=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://?Option-Name1=value")] + [DataRow(new[] { "test://?OPTION-NAME1=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://?OPTION-NAME1=value")] public void CaseInsensitive(string[] args, TestCommandLineStyle style) { // Arrange @@ -45,6 +47,22 @@ public void CaseInsensitive(string[] args, TestCommandLineStyle style) Assert.AreEqual("value", options.OptionName1); } + [TestMethod] + [DataRow(new[] { "test://?option-name1=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://?option-name1=value")] + [DataRow(new[] { "Test://?option-name1=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] Test://?option-name1=value")] + [DataRow(new[] { "TEST://?option-name1=value" }, TestCommandLineStyle.Url, DisplayName = "[Url] TEST://?option-name1=value")] + public void UrlCaseInsensitive(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual("value", options.OptionName1); + } + public record TestOptions { [Option("option-name1")] From 67ca36eb1eda427accd3bf467ad9438195410e61 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 09:00:44 +0800 Subject: [PATCH 104/193] =?UTF-8?q?=E8=AF=95=E5=9B=BE=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=90=84=E7=A7=8D=E7=B1=BB=E5=9E=8B=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 52 +++- .../ParsingStyles/PropertyTypeTests.cs | 237 ++++++++++++++++++ 2 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index da95ec38..fb866241 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -9,30 +9,64 @@ namespace DotNetCampus.CommandLine.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer { + /// + /// 允许的非泛型类型名称。 + /// private readonly HashSet _nonGenericTypeNames = [ - "String", "string", "Boolean", "bool", "Byte", "byte", "Int16", "short", "UInt16", "ushort", "Int32", "int", "UInt32", "uint", "Int64", "long", - "UInt64", "ulong", "Single", "float", "Double", "double", "Decimal", "decimal", "IList", "ICollection", "IEnumerable", + // bool + "bool", + "Boolean", + // number + "byte", "sbyte", "decimal", "double", "float", "int", "uint", "nint", "nuint", "long", "ulong", "short", "ushort", + "Byte", "SByte", "Decimal", "Double", "Single", "Int32", "UInt32", "IntPtr", "UIntPtr", "Int64", "UInt64", "Int16", "UInt16", + // string + "char", "string", + "Char", "String", ]; + /// + /// 允许的单泛型类型名称。 + /// private readonly HashSet _oneGenericTypeNames = [ - "[]", - "IList", "ICollection", "IEnumerable", "IReadOnlyList", "IReadOnlyCollection", "ISet", "IImmutableSet", "IImmutableList", - "ImmutableArray", "List", "ImmutableHashSet", "Collection", "HashSet", + // collection + "[]", "Collection", "List", "ReadOnlyCollection", "HashSet", "ImmutableArray", "ImmutableList", "ImmutableHashSet", + // sorted + "SortedList", "SortedSet", "ImmutableSortedSet", + // interface + "IEnumerable", "ICollection", "IList", "IReadOnlyCollection", "IReadOnlyList", "ISet", "IImmutableList", "IImmutableSet", ]; - private readonly HashSet _rawArgumentsGenericTypeNames = + /// + /// 允许的双泛型类型名称。 + /// + private readonly HashSet _twoGenericTypeNames = [ - "[]", "IList", "IReadOnlyList", "ICollection", "IReadOnlyCollection", "IEnumerable", + // dictionary + "KeyValuePair", "Dictionary", "ImmutableDictionary", + // sorted + "SortedDictionary", "SortedList", "ImmutableSortedDictionary", + // interface + "IDictionary", "IReadOnlyDictionary", ]; - private readonly HashSet _twoGenericTypeNames = + /// + /// 允许的 RawArguments 泛型类型名称。 + /// + private readonly HashSet _rawArgumentsGenericTypeNames = [ - "ImmutableDictionary", "Dictionary", "IDictionary", "IReadOnlyDictionary", "KeyValuePair", + "[]", "IList", "IReadOnlyList", "ICollection", "IReadOnlyCollection", "IEnumerable", ]; + /// + /// 允许的字典类型 Key 的泛型参数类型名称。 + /// private readonly HashSet _genericKeyArgumentTypeNames = ["String", "string"]; + + /// + /// 允许的泛型参数类型名称。 + /// private readonly HashSet _genericArgumentTypeNames = ["String", "string"]; /// diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs new file mode 100644 index 00000000..85c9b51a --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs @@ -0,0 +1,237 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; +using DotNetCampus.Cli.Compiler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class PropertyTypeTests +{ + [TestMethod] + public void SupportManyTypes() + { + // Arrange + // "--boolean-property", "true", + // "--immutable-array-property", "a,b,c", + string[] args = + [ + "--boolean-property", "true", + "--byte-property", "1", + "--sbyte-property", "1", + "--decimal-property", "1.1", + "--double-property", "1.1", + "--single-property", "1.1", + "--int32-property", "1", + "--uint32-property", "1", + "--intptr-property", "1", + "--uintptr-property", "1", + "--int64-property", "1", + "--uint64-property", "1", + "--int16-property", "1", + "--uint16-property", "1", + "--char-property", "a", + "--string-property", "value", + "--array-property", "a,b,c", + "--collection-property", "a,b,c", + "--read-only-collection-property", "a,b,c", + "--hash-set-property", "a,b,c", + "--immutable-array-property", "a,b,c", + "--immutable-list-property", "a,b,c", + "--immutable-hash-set-property", "a,b,c", + "--sorted-set-property", "a,b,c", + "--immutable-sorted-set-property", "a,b,c", + "--ienumerable-property", "a,b,c", + "--icollection-property", "a,b,c", + "--ilist-property", "a,b,c", + "--iread-only-collection-property", "a,b,c", + "--iread-only-list-property", "a,b,c", + "--iset-property", "a,b,c", + "--iimmutable-list-property", "a,b,c", + "--iimmutable-set-property", "a,b,c", + "--key-value-pair-property", "key:value", + "--dictionary-property", "key:value,key2:value2", + "--immutable-dictionary-property", "key:value,key2:value2", + "--sorted-dictionary-property", "key:value,key2:value2", + "--sorted-list-property", "key:value,key2:value2", + "--immutable-sorted-dictionary-property", "key:value,key2:value2", + "--idictionary-property", "key:value,key2:value2", + "--iread-only-dictionary-property", "key:value,key2:value2", + ]; + var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); + + // Act + var options = commandLine.As(); + + // Assert + Assert.IsNotNull(options); + Assert.AreEqual(true, options.BooleanProperty); + Assert.AreEqual((byte)1, options.ByteProperty); + Assert.AreEqual((sbyte)1, options.SByteProperty); + Assert.AreEqual(1.1m, options.DecimalProperty); + Assert.AreEqual(1.1, options.DoubleProperty); + Assert.AreEqual(1.1f, options.SingleProperty); + Assert.AreEqual(1, options.Int32Property); + Assert.AreEqual((uint)1, options.UInt32Property); + Assert.AreEqual((nint)1, options.IntPtrProperty); + Assert.AreEqual((nuint)1, options.UIntPtrProperty); + Assert.AreEqual((long)1, options.Int64Property); + Assert.AreEqual((ulong)1, options.UInt64Property); + Assert.AreEqual((short)1, options.Int16Property); + Assert.AreEqual((ushort)1, options.UInt16Property); + Assert.AreEqual('a', options.CharProperty); + Assert.AreEqual("value", options.StringProperty); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ArrayProperty); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.CollectionProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.ReadOnlyCollectionProperty!); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.HashSetProperty!.ToArray()); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ImmutableArrayProperty.ToArray()); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.ImmutableListProperty!); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.ImmutableHashSetProperty!.ToArray()); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.SortedSetProperty!.ToArray()); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.ImmutableSortedSetProperty!.ToArray()); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.IEnumerableProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.ICollectionProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.IListProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.IReadOnlyCollectionProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.IReadOnlyListProperty!); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.ISetProperty!.ToArray()); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.IImmutableListProperty!); + CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.IImmutableSetProperty!.ToArray()); + Assert.AreEqual(new KeyValuePair("key", "value"), options.KeyValuePairProperty); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, (ICollection)options.DictionaryProperty!); + } + + public record TestOptions + { + [Option] + public bool? BooleanProperty { get; set; } + + [Option] + public byte? ByteProperty { get; set; } + + [Option] + public sbyte? SByteProperty { get; set; } + + [Option] + public decimal? DecimalProperty { get; set; } + + [Option] + public double? DoubleProperty { get; set; } + + [Option] + public float? SingleProperty { get; set; } + + [Option] + public int? Int32Property { get; set; } + + [Option] + public uint? UInt32Property { get; set; } + + [Option] + public nint? IntPtrProperty { get; set; } + + [Option] + public nuint? UIntPtrProperty { get; set; } + + [Option] + public long? Int64Property { get; set; } + + [Option] + public ulong? UInt64Property { get; set; } + + [Option] + public short? Int16Property { get; set; } + + [Option] + public ushort? UInt16Property { get; set; } + + [Option] + public char? CharProperty { get; set; } + + [Option] + public string? StringProperty { get; set; } + + [Option] + public string[]? ArrayProperty { get; set; } + + [Option] + public Collection? CollectionProperty { get; set; } + + [Option] + public ReadOnlyCollection? ReadOnlyCollectionProperty { get; set; } + + [Option] + public HashSet? HashSetProperty { get; set; } + + [Option] + public ImmutableArray ImmutableArrayProperty { get; set; } + + [Option] + public ImmutableList? ImmutableListProperty { get; set; } + + [Option] + public ImmutableHashSet? ImmutableHashSetProperty { get; set; } + + [Option] + public SortedSet? SortedSetProperty { get; set; } + + [Option] + public ImmutableSortedSet? ImmutableSortedSetProperty { get; set; } + + [Option] + public IEnumerable? IEnumerableProperty { get; set; } + + [Option] + public ICollection? ICollectionProperty { get; set; } + + [Option] + public IList? IListProperty { get; set; } + + [Option] + public IReadOnlyCollection? IReadOnlyCollectionProperty { get; set; } + + [Option] + public IReadOnlyList? IReadOnlyListProperty { get; set; } + + [Option] + public ISet? ISetProperty { get; set; } + + [Option] + public IImmutableList? IImmutableListProperty { get; set; } + + [Option] + public IImmutableSet? IImmutableSetProperty { get; set; } + + [Option] + public KeyValuePair? KeyValuePairProperty { get; set; } + + [Option] + public Dictionary? DictionaryProperty { get; set; } + + [Option] + public ImmutableDictionary? ImmutableDictionaryProperty { get; set; } + + [Option] + public SortedDictionary? SortedDictionaryProperty { get; set; } + + [Option] + public SortedList? SortedListProperty { get; set; } + + [Option] + public ImmutableSortedDictionary? ImmutableSortedDictionaryProperty { get; set; } + + [Option] + public IDictionary? IDictionaryProperty { get; set; } + + [Option] + public IReadOnlyDictionary? IReadOnlyDictionaryProperty { get; set; } + } +} From 2c3de6ac47608999f58fa41da2b2ac8a39871c6b Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 11:58:36 +0800 Subject: [PATCH 105/193] =?UTF-8?q?=E6=95=B4=E7=90=86=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E5=85=B6=E6=94=AF=E6=8C=81=E7=9A=84=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=8C=E5=85=A8=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 220 ++-------------- .../CommandOptionPropertyTypeAnalysis.cs | 37 +++ .../CodeAnalysis/CommandPropertyTypeInfo.cs | 249 ++++++++++++++++++ .../CommandValueKind.cs | 2 +- .../Generators/ModelBuilderGenerator.cs | 7 +- .../Models/CommandObjectGeneratingModel.cs | 5 +- .../Models/GeneratingModelExtensions.cs | 115 +------- .../Compiler/PropertyAssignments.cs | 106 +++++++- .../ParsingStyles/PropertyTypeTests.cs | 10 - 9 files changed, 408 insertions(+), 343 deletions(-) create mode 100644 src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs create mode 100644 src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs rename src/DotNetCampus.CommandLine.Analyzer/{Generators/Models => CodeAnalysis}/CommandValueKind.cs (94%) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index fb866241..14b928b5 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using DotNetCampus.CommandLine.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,66 +10,6 @@ namespace DotNetCampus.CommandLine.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer { - /// - /// 允许的非泛型类型名称。 - /// - private readonly HashSet _nonGenericTypeNames = - [ - // bool - "bool", - "Boolean", - // number - "byte", "sbyte", "decimal", "double", "float", "int", "uint", "nint", "nuint", "long", "ulong", "short", "ushort", - "Byte", "SByte", "Decimal", "Double", "Single", "Int32", "UInt32", "IntPtr", "UIntPtr", "Int64", "UInt64", "Int16", "UInt16", - // string - "char", "string", - "Char", "String", - ]; - - /// - /// 允许的单泛型类型名称。 - /// - private readonly HashSet _oneGenericTypeNames = - [ - // collection - "[]", "Collection", "List", "ReadOnlyCollection", "HashSet", "ImmutableArray", "ImmutableList", "ImmutableHashSet", - // sorted - "SortedList", "SortedSet", "ImmutableSortedSet", - // interface - "IEnumerable", "ICollection", "IList", "IReadOnlyCollection", "IReadOnlyList", "ISet", "IImmutableList", "IImmutableSet", - ]; - - /// - /// 允许的双泛型类型名称。 - /// - private readonly HashSet _twoGenericTypeNames = - [ - // dictionary - "KeyValuePair", "Dictionary", "ImmutableDictionary", - // sorted - "SortedDictionary", "SortedList", "ImmutableSortedDictionary", - // interface - "IDictionary", "IReadOnlyDictionary", - ]; - - /// - /// 允许的 RawArguments 泛型类型名称。 - /// - private readonly HashSet _rawArgumentsGenericTypeNames = - [ - "[]", "IList", "IReadOnlyList", "ICollection", "IReadOnlyCollection", "IEnumerable", - ]; - - /// - /// 允许的字典类型 Key 的泛型参数类型名称。 - /// - private readonly HashSet _genericKeyArgumentTypeNames = ["String", "string"]; - - /// - /// 允许的泛型参数类型名称。 - /// - private readonly HashSet _genericArgumentTypeNames = ["String", "string"]; - /// /// Supported diagnostics. /// @@ -126,6 +67,7 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) { var isValidPropertyUsage = AnalyzeOptionPropertyType(context.SemanticModel, propertyNode); var diagnostic = CreateDiagnosticForTypeSyntax( + context.SemanticModel, isValidPropertyUsage ? Diagnostics.DCL201_SupportedOptionPropertyType : Diagnostics.DCL202_NotSupportedOptionPropertyType, @@ -141,6 +83,7 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) if (!isValidPropertyUsage) { var diagnostic = CreateDiagnosticForTypeSyntax( + context.SemanticModel, Diagnostics.DCL203_NotSupportedRawArgumentsPropertyType, propertyNode); context.ReportDiagnostic(diagnostic); @@ -150,17 +93,13 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) } } - private Diagnostic CreateDiagnosticForTypeSyntax(DiagnosticDescriptor rule, PropertyDeclarationSyntax propertySyntax) + private Diagnostic CreateDiagnosticForTypeSyntax(SemanticModel semanticModel, DiagnosticDescriptor rule, PropertyDeclarationSyntax propertySyntax) { - var typeSyntax = propertySyntax.Type; - if (typeSyntax is NullableTypeSyntax nullableTypeSyntax) - { - // string? - typeSyntax = nullableTypeSyntax.ElementType; - } - string typeName = GetTypeName(typeSyntax); - - return Diagnostic.Create(rule, typeSyntax.GetLocation(), typeName); + var typeSyntax = propertySyntax.Type is NullableTypeSyntax nullableTypeSyntax + ? nullableTypeSyntax.ElementType + : propertySyntax.Type; + var propertyTypeSymbol = (ITypeSymbol)semanticModel.GetSymbolInfo(propertySyntax.Type).Symbol!; + return Diagnostic.Create(rule, typeSyntax.GetLocation(), propertyTypeSymbol.GetSymbolInfoAsCommandProperty().GetSimpleName()); } /// @@ -171,44 +110,9 @@ private Diagnostic CreateDiagnosticForTypeSyntax(DiagnosticDescriptor rule, Prop /// private bool AnalyzeOptionPropertyType(SemanticModel semanticModel, PropertyDeclarationSyntax propertySyntax) { - var propertyTypeSyntax = propertySyntax.Type; - string typeName = GetTypeName(propertyTypeSyntax); - var (genericType0, genericType1) = GetGenericTypeNames(propertyTypeSyntax); - - if (IsTwoGenericType(typeName) - && genericType0 != null && genericType1 != null - && IsGenericKeyArgumentType(genericType0) - && IsGenericArgumentType(genericType1)) - { - return true; - } - - if (IsOneGenericType(typeName) - && genericType0 != null - && IsGenericArgumentType(genericType0)) - { - return true; - } - - if (IsNonGenericType(typeName)) - { - return true; - } - - if (propertyTypeSyntax is NullableTypeSyntax nullableTypeSyntax - && semanticModel.GetSymbolInfo(nullableTypeSyntax.ElementType).Symbol is INamedTypeSymbol { TypeKind: TypeKind.Enum }) - { - // Enum? - return true; - } - - if (semanticModel.GetSymbolInfo(propertyTypeSyntax).Symbol is INamedTypeSymbol { TypeKind: TypeKind.Enum }) - { - // Enum - return true; - } - - return false; + var propertyTypeSymbol = (ITypeSymbol)semanticModel.GetSymbolInfo(propertySyntax.Type).Symbol!; + var propertyInfo = propertyTypeSymbol.GetSymbolInfoAsCommandProperty(); + return propertyInfo.Kind is not CommandValueKind.Unknown; } /// @@ -219,102 +123,8 @@ private bool AnalyzeOptionPropertyType(SemanticModel semanticModel, PropertyDecl /// private bool AnalyzeRawArgumentsPropertyType(SemanticModel semanticModel, PropertyDeclarationSyntax propertySyntax) { - var propertyTypeSyntax = propertySyntax.Type; - string typeName = GetTypeName(propertyTypeSyntax); - var (genericType0, genericType1) = GetGenericTypeNames(propertyTypeSyntax); - - if (IsRawArgumentsGenericType(typeName) - && genericType0 != null - && IsGenericArgumentType(genericType0)) - { - return true; - } - - return false; + var propertyTypeSymbol = (ITypeSymbol)semanticModel.GetSymbolInfo(propertySyntax.Type).Symbol!; + var propertyInfo = propertyTypeSymbol.GetSymbolInfoAsCommandProperty(); + return propertyInfo.IsAssignableFromArrayOrList(); } - - private string GetTypeName(TypeSyntax typeSyntax) - { - if (typeSyntax is NullableTypeSyntax nullableTypeSyntax) - { - // string? - typeSyntax = nullableTypeSyntax.ElementType; - } - - if (typeSyntax is GenericNameSyntax genericNameSyntax) - { - // List - // Dictionary - return genericNameSyntax.Identifier.ToString(); - } - - if (typeSyntax is ArrayTypeSyntax) - { - // string[] - return "[]"; - } - - if (typeSyntax is PredefinedTypeSyntax predefinedTypeSyntax) - { - // string - return predefinedTypeSyntax.ToString(); - } - - if (typeSyntax is QualifiedNameSyntax qualifiedNameSyntax) - { - // System.String - return qualifiedNameSyntax.ChildNodes().OfType().Last().ToString(); - } - - // String - return typeSyntax.ToString(); - } - - private (string?, string?) GetGenericTypeNames(TypeSyntax typeSyntax) - { - if (typeSyntax is NullableTypeSyntax nullableTypeSyntax) - { - // string? - typeSyntax = nullableTypeSyntax.ElementType; - } - - string? genericType0 = null, genericType1 = null; - if (typeSyntax is GenericNameSyntax genericNameSyntax) - { - var genericTypes = genericNameSyntax.TypeArgumentList.ChildNodes().OfType().ToList(); - genericType0 = GetTypeName(genericTypes[0]); - if (genericTypes.Count == 2) - { - genericType1 = GetTypeName(genericTypes[1]); - } - else if (genericTypes.Count > 2) - { - genericType0 = null; - genericType1 = null; - } - } - else if (typeSyntax is ArrayTypeSyntax arrayTypeSyntax) - { - genericType0 = GetTypeName(arrayTypeSyntax.ElementType); - } - return (genericType0, genericType1); - } - - private bool IsNonGenericType(string typeName) - => _nonGenericTypeNames.Contains(typeName); - - private bool IsOneGenericType(string typeName) - => _oneGenericTypeNames.Contains(typeName); - - private bool IsRawArgumentsGenericType(string typeName) - => _rawArgumentsGenericTypeNames.Contains(typeName); - - private bool IsTwoGenericType(string typeName) - => _twoGenericTypeNames.Contains(typeName); - - private bool IsGenericKeyArgumentType(string typeName) - => _genericKeyArgumentTypeNames.Contains(typeName); - - private bool IsGenericArgumentType(string typeName) - => _genericArgumentTypeNames.Contains(typeName); } diff --git a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs new file mode 100644 index 00000000..e12d570a --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.CodeAnalysis; + +/// +/// 辅助分析及生成命令行对象属性所支持的类型。 +/// +internal static class CommandOptionPropertyTypeAnalysis +{ + /// + /// 如果 是可空值类型,则递归返回其基础类型,否则直接返回 本身。
+ /// 不会处理其泛型参数的可空性。 + ///
+ /// 要处理的类型符号。 + /// 基础类型符号。 + public static ITypeSymbol GetNotNullTypeSymbol(this ITypeSymbol typeSymbol) => typeSymbol switch + { + INamedTypeSymbol + { + IsValueType: true, + IsGenericType: true, + OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, + } nullableTypeSymbol => nullableTypeSymbol.TypeArguments[0], + _ => typeSymbol, + }; + + /// + /// 视 为命令行属性的类型,按命令行属性的要求获取其所需的类型信息。
+ /// 这个过程会丢掉类型的可空性信息。 + ///
+ /// 类型符号。 + /// 类型信息。 + public static CommandPropertyTypeInfo GetSymbolInfoAsCommandProperty(this ITypeSymbol typeSymbol) + { + return new CommandPropertyTypeInfo(typeSymbol); + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs new file mode 100644 index 00000000..ad2ef069 --- /dev/null +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs @@ -0,0 +1,249 @@ +using Microsoft.CodeAnalysis; + +namespace DotNetCampus.CommandLine.CodeAnalysis; + +/// +/// 命令行属性的类型信息。 +/// +internal record CommandPropertyTypeInfo +{ + public CommandPropertyTypeInfo(ITypeSymbol typeSymbol) + { + TypeSymbol = typeSymbol.GetNotNullTypeSymbol(); + Kind = GetSymbolInfoAsCommandProperty(typeSymbol); + } + + /// + /// 获取类型的简单名称,仅包含名称本身,不包含命名空间、泛型参数、可空标记等信息。 + /// + /// + public string GetSimpleName() => TypeSymbol.ToDisplayString(SimpleNameFormat); + + public ITypeSymbol TypeSymbol { get; } + + public CommandValueKind Kind { get; } + + /// + /// 获取当前类型是否可以从数组或列表赋值。 + /// + /// 如果可以从数组或列表赋值,则返回 ;否则返回 + public bool IsAssignableFromArrayOrList() + { + if (Kind is not CommandValueKind.List) + { + return false; + } + + if (TypeSymbol.Kind is SymbolKind.ArrayType) + { + return true; + } + + var simpleName = GetSimpleName(); + return AllowedArrayOrListTypeNames.Contains(simpleName); + } + + /// + /// 如果当前类型是枚举类型,则返回其枚举类型符号,否则返回 。 + /// + /// 枚举类型符号,或 + public ITypeSymbol? AsEnumSymbol() + { + return Kind is not CommandValueKind.Enum ? null : TypeSymbol; + } + + /// + /// 获取类型的非抽象名称。
+ /// 对于命令行解析中所支持的各种接口,会被映射为其常见的具体类型名称。 + ///
+ /// 非抽象名称。 + public string GetGeneratedNotAbstractTypeName() + { + if (TypeSymbol.Kind is SymbolKind.ArrayType) + { + return "Array"; + } + + return Kind switch + { + CommandValueKind.Boolean => TypeSymbol.ToDisplayString(SimpleDeclarationNameFormat), + CommandValueKind.Number => TypeSymbol.ToDisplayString(SimpleDeclarationNameFormat), + CommandValueKind.Enum => "Enum", + CommandValueKind.String => TypeSymbol.ToDisplayString(SimpleDeclarationNameFormat), + CommandValueKind.List => AllowedListTypeNames.TryGetValue(GetSimpleName(), out var list) + ? list + : "List", + CommandValueKind.Dictionary => AllowedDictionaryTypeNames.TryGetValue(GetSimpleName(), out var dict) + ? dict + : "Dictionary", + _ => "Unknown", + }; + } + + /// + /// 允许的单泛型类型名称。 + /// + /// + /// 允许的单泛型类型名称。 + /// + private static readonly Dictionary AllowedListTypeNames = new Dictionary + { + ["Collection"] = "Collection", + ["HashSet"] = "HashSet", + ["ICollection"] = "List", + ["IEnumerable"] = "List", + ["IImmutableList"] = "ImmutableList", + ["IImmutableSet"] = "ImmutableHashSet", + ["IList"] = "List", + ["ImmutableArray"] = "ImmutableArray", + ["ImmutableHashSet"] = "ImmutableHashSet", + ["ImmutableList"] = "ImmutableList", + ["ImmutableSortedSet"] = "ImmutableSortedSet", + ["IReadOnlyCollection"] = "List", + ["IReadOnlyList"] = "List", + ["ISet"] = "HashSet", + ["List"] = "List", + ["ReadOnlyCollection"] = "ReadOnlyCollection", + ["SortedSet"] = "SortedSet", + }; + + /// + /// 允许的双泛型类型名称。 + /// + private static readonly Dictionary AllowedDictionaryTypeNames = new Dictionary + { + ["Dictionary"] = "Dictionary", + ["IDictionary"] = "Dictionary", + ["ImmutableDictionary"] = "ImmutableDictionary", + ["ImmutableSortedDictionary"] = "ImmutableSortedDictionary", + ["IReadOnlyDictionary"] = "Dictionary", + ["KeyValuePair"] = "KeyValuePair", + ["SortedDictionary"] = "SortedDictionary", + }; + + /// + /// 允许的 RawArguments 泛型类型名称。 + /// + private static readonly HashSet AllowedArrayOrListTypeNames = + [ + "IList", "IReadOnlyList", "ICollection", "IReadOnlyCollection", "IEnumerable", + ]; + + /// + /// 用于将类型符号转换为仅包含名称的字符串形式。会去掉可空标记、命名空间、泛型参数等信息。 + /// + private static readonly SymbolDisplayFormat SimpleNameFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.None, + kindOptions: SymbolDisplayKindOptions.None, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes + ); + + private static readonly SymbolDisplayFormat SimpleDeclarationNameFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.None, + kindOptions: SymbolDisplayKindOptions.None + ); + + /// + /// 视 为命令行属性的类型,按命令行属性的要求获取其所需的类型信息。
+ /// 这个过程会丢掉类型的可空性信息。 + ///
+ /// 类型符号。 + /// 类型信息。 + private static CommandValueKind GetSymbolInfoAsCommandProperty(ITypeSymbol typeSymbol) + { + var notNullTypeSymbol = typeSymbol.GetNotNullTypeSymbol(); + + switch (notNullTypeSymbol.SpecialType) + { + case SpecialType.System_Boolean: + return CommandValueKind.Boolean; + case SpecialType.System_Byte: + case SpecialType.System_SByte: + case SpecialType.System_Decimal: + case SpecialType.System_Double: + case SpecialType.System_Single: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + // 不应支持这种不能跨进程传递的类型。 + // case SpecialType.System_IntPtr: + // case SpecialType.System_UIntPtr: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + return CommandValueKind.Number; + case SpecialType.System_Char: + case SpecialType.System_String: + return CommandValueKind.String; + case SpecialType.System_Array: + case SpecialType.System_Collections_IEnumerable: + case SpecialType.System_Collections_Generic_IEnumerable_T: + case SpecialType.System_Collections_Generic_IList_T: + case SpecialType.System_Collections_Generic_ICollection_T: + case SpecialType.System_Collections_IEnumerator: + case SpecialType.System_Collections_Generic_IEnumerator_T: + case SpecialType.System_Collections_Generic_IReadOnlyList_T: + case SpecialType.System_Collections_Generic_IReadOnlyCollection_T: + return CommandValueKind.List; + case SpecialType.None: + // 其他类型,进行后续分析。 + break; + default: + return CommandValueKind.Unknown; + } + + if (notNullTypeSymbol.TypeKind is TypeKind.Enum) + { + return CommandValueKind.Enum; + } + + // List + if (typeSymbol is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_String }) + { + return CommandValueKind.List; + } + + // List + if (notNullTypeSymbol is INamedTypeSymbol + { + TypeArguments: [{ SpecialType: SpecialType.System_String }], + OriginalDefinition.Name: { } oneGenericName, + } && AllowedListTypeNames.ContainsKey(oneGenericName)) + { + return CommandValueKind.List; + } + + // Dictionary + if (notNullTypeSymbol is INamedTypeSymbol + { + TypeArguments: [{ SpecialType: SpecialType.System_String }, { SpecialType: SpecialType.System_String }], + OriginalDefinition.Name: { } twoGenericName, + } && AllowedDictionaryTypeNames.ContainsKey(twoGenericName)) + { + return CommandValueKind.Dictionary; + } + + return CommandValueKind.Unknown; + } +} + +public static class CommandPropertyTypeInfoExtensions +{ + /// + /// 假定 是一个命令行对象中一个枚举属性的属性类型, + /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, + /// 此方法返回这个辅助类型的名称。 + /// + /// 命令行对象中一个枚举属性的属性类型。 + /// 辅助类型的名称。 + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + { + return symbol.GetSymbolInfoAsCommandProperty().AsEnumSymbol() is { } enumTypeSymbol + ? $"__GeneratedEnumArgument__{enumTypeSymbol.ToDisplayString().Replace('.', '_')}__" + : symbol.ToDisplayString(); + } +} diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandValueKind.cs similarity index 94% rename from src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandValueKind.cs index bdf90d21..825be56f 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandValueKind.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandValueKind.cs @@ -1,4 +1,4 @@ -namespace DotNetCampus.CommandLine.Generators.Models; +namespace DotNetCampus.CommandLine.CodeAnalysis; /// /// 从命令行解析参数的含义时,对于值(选项和位置参数)的类型的分类。
diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index e66d1d0e..ce6db414 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -1,4 +1,5 @@ using System.Text; +using DotNetCampus.CommandLine.CodeAnalysis; using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; using DotNetCampus.CommandLine.Generators.Models; @@ -425,7 +426,7 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau // | 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | // | 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | - var toTarget = model.Type.ToCommandValueNonAbstractName(); + var toTarget = model.Type.GetGeneratedNotAbstractTypeName(); var isList = model.Type.AsCommandValueKind() is CommandValueKind.List or CommandValueKind.Dictionary; var fallback = (model.IsRequired, model.IsInitOnly, isList, model.IsNullable) switch { @@ -474,7 +475,7 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex) { - var toTarget = model.Type.ToCommandValueNonAbstractName(); + var toTarget = model.Type.GetGeneratedNotAbstractTypeName(); var variablePrefix = model switch { RawArgumentPropertyGeneratingModel => "a", @@ -545,7 +546,7 @@ private string GenerateEnumDeclarationCode(ITypeSymbol enumType) /// /// Converts the parsed value to the enum type. /// - public {{enumType.ToUsingString()}}? To{{enumType.ToCommandValueNonAbstractName()}}() => Value; + public {{enumType.ToUsingString()}}? To{{enumType.GetGeneratedNotAbstractTypeName()}}() => Value; } """; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs index a7b27172..0d4a29d2 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -1,4 +1,5 @@ using DotNetCampus.Cli.Utils; +using DotNetCampus.CommandLine.CodeAnalysis; using Microsoft.CodeAnalysis; namespace DotNetCampus.CommandLine.Generators.Models; @@ -70,14 +71,14 @@ public IEnumerable EnumerateEnumPropertyTypes() foreach (var option in OptionProperties) { - if (option.Type.TryGetEnumType(out var enumTypeSymbol)) + if (option.Type.GetSymbolInfoAsCommandProperty().AsEnumSymbol() is { } enumTypeSymbol) { enums.Add(enumTypeSymbol); } } foreach (var value in PositionalArgumentProperties) { - if (value.Type.TryGetEnumType(out var enumTypeSymbol)) + if (value.Type.GetSymbolInfoAsCommandProperty().AsEnumSymbol() is { } enumTypeSymbol) { enums.Add(enumTypeSymbol); } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs index 41dce3eb..0352fdc1 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs @@ -1,3 +1,4 @@ +using DotNetCampus.CommandLine.CodeAnalysis; using Microsoft.CodeAnalysis; namespace DotNetCampus.CommandLine.Generators.Models; @@ -14,43 +15,6 @@ internal static class GeneratingModelExtensions kindOptions: SymbolDisplayKindOptions.None ); - /// - /// 尝试判断 是否是一个枚举类型(含可空枚举类型)。 - /// 如果是,则返回 ,并通过 返回枚举类型本身。 - /// 否则返回 ,并将 设为 。 - /// - /// 命令行对象中一个枚举属性的属性类型。 - /// 如果返回值为 ,则为枚举类型本身;否则为 。 - /// 辅助类型的名称。 - public static bool TryGetEnumType(this ITypeSymbol symbol, out ITypeSymbol enumTypeSymbol) - { - if (symbol is { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) - { - enumTypeSymbol = typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType - // 获取 Nullable 中的 T。 - ? namedType.TypeArguments[0] - // 处理直接带有可空标记的类型 (int? 这种形式)。 - : typeSymbol; - return enumTypeSymbol.TypeKind is TypeKind.Enum; - } - enumTypeSymbol = symbol; - return symbol.TypeKind is TypeKind.Enum; - } - - /// - /// 假定 是一个命令行对象中一个枚举属性的属性类型, - /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, - /// 此方法返回这个辅助类型的名称。 - /// - /// 命令行对象中一个枚举属性的属性类型。 - /// 辅助类型的名称。 - public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) - { - return symbol.TryGetEnumType(out var enumTypeSymbol) - ? $"__GeneratedEnumArgument__{enumTypeSymbol.ToDisplayString().Replace('.', '_')}__" - : symbol.ToDisplayString(); - } - public static string ToCommandValueTypeName(this CommandValueKind type) => type switch { CommandValueKind.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", @@ -65,29 +29,9 @@ public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) ///
/// 类型符号。 /// 非抽象名称。 - public static string ToCommandValueNonAbstractName(this ITypeSymbol typeSymbol) + public static string GetGeneratedNotAbstractTypeName(this ITypeSymbol typeSymbol) { - if (typeSymbol.Kind is SymbolKind.ArrayType) - { - return "Array"; - } - - var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); - if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) - { - // Nullable 类型 - var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return ToCommandValueNonAbstractName(genericType); - } - - // 取出类型的 .NET 类名称,不含泛型。如 bool 返回 Boolean,Dictionary 返回 Dictionary。 - return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch - { - "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" - or "IImmutableSet" or "IImmutableList" => "List", - "IDictionary" or "IReadOnlyDictionary" => "Dictionary", - var name => name, - }; + return typeSymbol.GetSymbolInfoAsCommandProperty().GetGeneratedNotAbstractTypeName(); } /// @@ -97,58 +41,7 @@ public static string ToCommandValueNonAbstractName(this ITypeSymbol typeSymbol) /// 命令行值的种类。 public static CommandValueKind AsCommandValueKind(this ITypeSymbol typeSymbol) { - if (typeSymbol.SpecialType is SpecialType.System_Boolean) - { - return CommandValueKind.Boolean; - } - - if (typeSymbol.SpecialType is SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_Int32 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Single or - SpecialType.System_Double or - SpecialType.System_Decimal) - { - return CommandValueKind.Number; - } - - if (typeSymbol.TypeKind is TypeKind.Enum) - { - return CommandValueKind.Enum; - } - - if (typeSymbol.SpecialType is SpecialType.System_String) - { - return CommandValueKind.String; - } - - if (typeSymbol.Kind is SymbolKind.ArrayType) - { - return CommandValueKind.List; - } - - var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); - if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) - { - // Nullable 类型 - var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return AsCommandValueKind(genericType); - } - - return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch - { - "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" - or "IImmutableSet" or "IImmutableList" => CommandValueKind.List, - "ImmutableArray" or "List" or "ImmutableHashSet" or "Collection" or "HashSet" => CommandValueKind.List, - "IDictionary" or "IReadOnlyDictionary" => CommandValueKind.Dictionary, - "ImmutableDictionary" or "Dictionary" or "KeyValuePair" => CommandValueKind.Dictionary, - _ => CommandValueKind.Unknown, - }; + return typeSymbol.GetSymbolInfoAsCommandProperty().Kind; } /// diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index 5288cf83..a42e901d 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -217,6 +217,15 @@ public StringListArgument Append(ReadOnlySpan value) return new StringListArgument { Value = list }; } + /// + /// 将解析到的值转换为集合。 + /// + public Collection ToCollection() => Value switch + { + null or { Count: 0 } => [], + { } values => [..values], + }; + /// /// 将解析到的值转换为字符串数组。 /// @@ -226,6 +235,42 @@ public StringListArgument Append(ReadOnlySpan value) { } values => [..values], }; + /// + /// 将解析到的值转换为哈希集合。 + /// + public HashSet ToHashSet() => Value switch + { + null or { Count: 0 } => [], + { } values => [..values], + }; + + /// + /// 将解析到的值转换为列表。 + /// + public List ToList() => Value switch + { + null or { Count: 0 } => [], + { } values => values, + }; + + /// + /// 将解析到的值转换为只读集合。 + /// + public ReadOnlyCollection ToReadOnlyCollection() => Value switch + { + null or { Count: 0 } => new ReadOnlyCollection([]), + { } values => new ReadOnlyCollection(values), + }; + + /// + /// 将解析到的值转换为排序集合。 + /// + public SortedSet ToSortedSet() => Value switch + { + null or { Count: 0 } => [], + { } values => [..values], + }; + #if NETCOREAPP3_1_OR_GREATER /// /// 将解析到的值转换为不可变数组。 @@ -242,38 +287,48 @@ public StringListArgument Append(ReadOnlySpan value) }; /// - /// 将解析到的值转换为不可变哈希集合。 + /// 将解析到的值转换为不可变列表。 /// - public ImmutableHashSet ToImmutableHashSet() => Value switch + public ImmutableList ToImmutableList() => Value switch { #if NET8_0_OR_GREATER null or { Count: 0 } => [], { } values => [..values], #else - null or { Count: 0 } => ImmutableHashSet.Empty, - { } values => values.ToImmutableHashSet(), + null or { Count: 0 } => ImmutableList.Empty, + { } values => values.ToImmutableList(), #endif }; -#endif - /// - /// 将解析到的值转换为集合。 + /// 将解析到的值转换为不可变排序集合。 /// - public Collection ToCollection() => Value switch + public ImmutableSortedSet ToImmutableSortedSet() => Value switch { +#if NET8_0_OR_GREATER null or { Count: 0 } => [], { } values => [..values], +#else + null or { Count: 0 } => ImmutableSortedSet.Empty, + { } values => values.ToImmutableSortedSet(), +#endif }; /// - /// 将解析到的值转换为列表。 + /// 将解析到的值转换为不可变哈希集合。 /// - public List ToList() => Value switch + public ImmutableHashSet ToImmutableHashSet() => Value switch { +#if NET8_0_OR_GREATER null or { Count: 0 } => [], - { } values => values, + { } values => [..values], +#else + null or { Count: 0 } => ImmutableHashSet.Empty, + { } values => values.ToImmutableHashSet(), +#endif }; + +#endif } /// @@ -327,6 +382,35 @@ public Dictionary ToDictionary() { return Value ?? []; } + + /// + /// 将解析到的值转换为排序字典。 + /// + public SortedDictionary ToSortedDictionary() => Value switch + { + null or { Count: 0 } => new SortedDictionary(), + { } values => new SortedDictionary(values), + }; + +#if NETCOREAPP3_1_OR_GREATER + /// + /// 将解析到的值转换为不可变字典。 + /// + public ImmutableDictionary ToImmutableDictionary() => Value switch + { + null or { Count: 0 } => ImmutableDictionary.Empty, + { } values => values.ToImmutableDictionary(), + }; + + /// + /// 将解析到的值转换为不可变排序字典。 + /// + public ImmutableSortedDictionary ToImmutableSortedDictionary() => Value switch + { + null or { Count: 0 } => ImmutableSortedDictionary.Empty, + { } values => values.ToImmutableSortedDictionary(), + }; +#endif } /// diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs index 85c9b51a..c4d206ca 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs @@ -27,8 +27,6 @@ public void SupportManyTypes() "--single-property", "1.1", "--int32-property", "1", "--uint32-property", "1", - "--intptr-property", "1", - "--uintptr-property", "1", "--int64-property", "1", "--uint64-property", "1", "--int16-property", "1", @@ -76,8 +74,6 @@ public void SupportManyTypes() Assert.AreEqual(1.1f, options.SingleProperty); Assert.AreEqual(1, options.Int32Property); Assert.AreEqual((uint)1, options.UInt32Property); - Assert.AreEqual((nint)1, options.IntPtrProperty); - Assert.AreEqual((nuint)1, options.UIntPtrProperty); Assert.AreEqual((long)1, options.Int64Property); Assert.AreEqual((ulong)1, options.UInt64Property); Assert.AreEqual((short)1, options.Int16Property); @@ -135,12 +131,6 @@ public record TestOptions [Option] public uint? UInt32Property { get; set; } - [Option] - public nint? IntPtrProperty { get; set; } - - [Option] - public nuint? UIntPtrProperty { get; set; } - [Option] public long? Int64Property { get; set; } From 685b0fdf8285dfbd75ef7e0748991a6a4b1a3bba Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 12:16:24 +0800 Subject: [PATCH 106/193] =?UTF-8?q?=E6=95=B4=E7=90=86=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindOptionPropertyTypeAnalyzer.cs | 1 + .../CommandOptionPropertyTypeAnalysis.cs | 37 ------------ .../CodeAnalysis/CommandPropertyTypeInfo.cs | 44 ++++++++------ .../Generators/ModelBuilderGenerator.cs | 2 +- .../Models/GeneratingModelExtensions.cs | 58 ++++++++++--------- .../Compiler/PropertyAssignments.cs | 25 ++++++++ .../ParsingStyles/PropertyTypeTests.cs | 4 +- 7 files changed, 85 insertions(+), 86 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index 14b928b5..5fbc3127 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using DotNetCampus.CommandLine.CodeAnalysis; +using DotNetCampus.CommandLine.Generators.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs deleted file mode 100644 index e12d570a..00000000 --- a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandOptionPropertyTypeAnalysis.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace DotNetCampus.CommandLine.CodeAnalysis; - -/// -/// 辅助分析及生成命令行对象属性所支持的类型。 -/// -internal static class CommandOptionPropertyTypeAnalysis -{ - /// - /// 如果 是可空值类型,则递归返回其基础类型,否则直接返回 本身。
- /// 不会处理其泛型参数的可空性。 - ///
- /// 要处理的类型符号。 - /// 基础类型符号。 - public static ITypeSymbol GetNotNullTypeSymbol(this ITypeSymbol typeSymbol) => typeSymbol switch - { - INamedTypeSymbol - { - IsValueType: true, - IsGenericType: true, - OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, - } nullableTypeSymbol => nullableTypeSymbol.TypeArguments[0], - _ => typeSymbol, - }; - - /// - /// 视 为命令行属性的类型,按命令行属性的要求获取其所需的类型信息。
- /// 这个过程会丢掉类型的可空性信息。 - ///
- /// 类型符号。 - /// 类型信息。 - public static CommandPropertyTypeInfo GetSymbolInfoAsCommandProperty(this ITypeSymbol typeSymbol) - { - return new CommandPropertyTypeInfo(typeSymbol); - } -} diff --git a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs index ad2ef069..8eeb15f7 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeAnalysis/CommandPropertyTypeInfo.cs @@ -9,19 +9,25 @@ internal record CommandPropertyTypeInfo { public CommandPropertyTypeInfo(ITypeSymbol typeSymbol) { - TypeSymbol = typeSymbol.GetNotNullTypeSymbol(); + TypeSymbol = GetNotNullTypeSymbol(typeSymbol); Kind = GetSymbolInfoAsCommandProperty(typeSymbol); } + public ITypeSymbol TypeSymbol { get; } + + public CommandValueKind Kind { get; } + /// - /// 获取类型的简单名称,仅包含名称本身,不包含命名空间、泛型参数、可空标记等信息。 + /// 获取类型的简单名称,仅包含名称本身,不包含命名空间、泛型参数、可空标记等信息,尽可能使用类型关键字。 /// /// public string GetSimpleName() => TypeSymbol.ToDisplayString(SimpleNameFormat); - public ITypeSymbol TypeSymbol { get; } - - public CommandValueKind Kind { get; } + /// + /// 获取类型的简单名称,仅包含名称本身,不包含命名空间、泛型参数、可空标记等信息,尽可能使用类型名称而不是关键字。 + /// + /// + public string GetSimpleDeclarationName() => TypeSymbol.ToDisplayString(SimpleDeclarationNameFormat); /// /// 获取当前类型是否可以从数组或列表赋值。 @@ -155,7 +161,7 @@ public string GetGeneratedNotAbstractTypeName() /// 类型信息。 private static CommandValueKind GetSymbolInfoAsCommandProperty(ITypeSymbol typeSymbol) { - var notNullTypeSymbol = typeSymbol.GetNotNullTypeSymbol(); + var notNullTypeSymbol = GetNotNullTypeSymbol(typeSymbol); switch (notNullTypeSymbol.SpecialType) { @@ -229,21 +235,21 @@ private static CommandValueKind GetSymbolInfoAsCommandProperty(ITypeSymbol typeS return CommandValueKind.Unknown; } -} -public static class CommandPropertyTypeInfoExtensions -{ /// - /// 假定 是一个命令行对象中一个枚举属性的属性类型, - /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, - /// 此方法返回这个辅助类型的名称。 + /// 如果 是可空值类型,则递归返回其基础类型,否则直接返回 本身。
+ /// 不会处理其泛型参数的可空性。 ///
- /// 命令行对象中一个枚举属性的属性类型。 - /// 辅助类型的名称。 - public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + /// 要处理的类型符号。 + /// 基础类型符号。 + private static ITypeSymbol GetNotNullTypeSymbol(ITypeSymbol typeSymbol) => typeSymbol switch { - return symbol.GetSymbolInfoAsCommandProperty().AsEnumSymbol() is { } enumTypeSymbol - ? $"__GeneratedEnumArgument__{enumTypeSymbol.ToDisplayString().Replace('.', '_')}__" - : symbol.ToDisplayString(); - } + INamedTypeSymbol + { + IsValueType: true, + IsGenericType: true, + OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, + } nullableTypeSymbol => nullableTypeSymbol.TypeArguments[0], + _ => typeSymbol, + }; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index ce6db414..ff0d88b6 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -108,7 +108,7 @@ private string GenerateArgumentPropertyCode(PropertyGeneratingModel model) => CommandValueKind.String => "global::DotNetCampus.Cli.Compiler.StringArgument", CommandValueKind.List => "global::DotNetCampus.Cli.Compiler.StringListArgument", CommandValueKind.Dictionary => "global::DotNetCampus.Cli.Compiler.StringDictionaryArgument", - _ => $"// 不支持解析类型为 {model.Type.ToDisplayString()} 的属性 {model.PropertyName}。", + _ => "global::DotNetCampus.Cli.Compiler.ErrorArgument", }; private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $$""" diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs index 0352fdc1..0e2bd374 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/GeneratingModelExtensions.cs @@ -8,13 +8,6 @@ namespace DotNetCampus.CommandLine.Generators.Models; ///
internal static class GeneratingModelExtensions { - private static readonly SymbolDisplayFormat ToTargetTypeFormat = new SymbolDisplayFormat( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.None, - kindOptions: SymbolDisplayKindOptions.None - ); - public static string ToCommandValueTypeName(this CommandValueKind type) => type switch { CommandValueKind.Boolean => "global::DotNetCampus.Cli.OptionValueType.Boolean", @@ -23,6 +16,17 @@ internal static class GeneratingModelExtensions _ => "global::DotNetCampus.Cli.OptionValueType.Normal", }; + /// + /// 视 为命令行属性的类型,按命令行属性的要求获取其所需的类型信息。
+ /// 这个过程会丢掉类型的可空性信息。 + ///
+ /// 类型符号。 + /// 类型信息。 + public static CommandPropertyTypeInfo GetSymbolInfoAsCommandProperty(this ITypeSymbol typeSymbol) + { + return new CommandPropertyTypeInfo(typeSymbol); + } + /// /// 获取类型的非抽象名称。
/// 对于命令行解析中所支持的各种接口,会被映射为其常见的具体类型名称。 @@ -34,6 +38,20 @@ public static string GetGeneratedNotAbstractTypeName(this ITypeSymbol typeSymbol return typeSymbol.GetSymbolInfoAsCommandProperty().GetGeneratedNotAbstractTypeName(); } + /// + /// 假定 是一个命令行对象中一个枚举属性的属性类型, + /// 现在我们要为这个枚举生成一个用来赋值命令行值的辅助类型, + /// 此方法返回这个辅助类型的名称。 + /// + /// 命令行对象中一个枚举属性的属性类型。 + /// 辅助类型的名称。 + public static string GetGeneratedEnumArgumentTypeName(this ITypeSymbol symbol) + { + return symbol.GetSymbolInfoAsCommandProperty().AsEnumSymbol() is { } enumTypeSymbol + ? $"__GeneratedEnumArgument__{enumTypeSymbol.ToDisplayString().Replace('.', '_')}__" + : symbol.ToDisplayString(); + } + /// /// 将类型符号映射为命令行值的种类。 /// @@ -52,27 +70,15 @@ public static CommandValueKind AsCommandValueKind(this ITypeSymbol typeSymbol) /// 如果类型确定支持集合表达式,则返回 ;否则返回 public static bool SupportCollectionExpression(this ITypeSymbol typeSymbol, bool supportImmutableCollections) { - if (typeSymbol.Kind is SymbolKind.ArrayType) + var info = typeSymbol.GetSymbolInfoAsCommandProperty(); + if (info.Kind is not CommandValueKind.List) { - return true; + return false; } - var originalDefinitionString = typeSymbol.OriginalDefinition.ToString(); - if (originalDefinitionString.Equals("System.Nullable", StringComparison.Ordinal)) - { - // Nullable 类型 - var genericType = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return SupportCollectionExpression(genericType, supportImmutableCollections); - } - - return typeSymbol.ToDisplayString(ToTargetTypeFormat) switch - { - "IList" or "ICollection" or "IEnumerable" or "IReadOnlyList" or "IReadOnlyCollection" or "ISet" - or "IImmutableSet" or "IImmutableList" => true, - "List" or "Collection" or "HashSet" => true, - // 不可变集合在 .NET 8 及以上版本中支持集合表达式。 - "ImmutableArray" or "ImmutableHashSet" => supportImmutableCollections, - _ => false, - }; + // 不可变集合在 .NET 8 及以上版本中支持集合表达式。 + // 其他类型均直接支持集合表达式。 + var simpleName = info.GetSimpleDeclarationName(); + return !simpleName.Contains("Immutable") || supportImmutableCollections; } } diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index a42e901d..d5d921a1 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -3,6 +3,7 @@ #endif using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using DotNetCampus.Cli.Exceptions; namespace DotNetCampus.Cli.Compiler; @@ -413,6 +414,30 @@ public Dictionary ToDictionary() #endif } +/// +/// 专门解析来自命令行的错误参数,并假装赋值给属性。 +/// +public readonly record struct ErrorArgument +{ + /// + /// 当命令行属性赋值不受支持时,调用此方法假装赋值。 + /// + /// 传什么值进来都当作没看见。 + public ErrorArgument Assign(ReadOnlySpan value) + { + return this; + } + + /// + /// 将解析到的值转换为字符串。 + /// + [DoesNotReturn] + public object ToUnknown() + { + throw new CommandLineParseValueException("命令行属性赋值不受支持。"); + } +} + /// /// 在运行时解析来自命令行的枚举类型,并辅助赋值给属性。 /// diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs index c4d206ca..346c1029 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs @@ -5,6 +5,7 @@ using System.Linq; using DotNetCampus.Cli.Compiler; using Microsoft.VisualStudio.TestTools.UnitTesting; +// ReSharper disable InconsistentNaming namespace DotNetCampus.Cli.Tests.ParsingStyles; @@ -212,9 +213,6 @@ public record TestOptions [Option] public SortedDictionary? SortedDictionaryProperty { get; set; } - [Option] - public SortedList? SortedListProperty { get; set; } - [Option] public ImmutableSortedDictionary? ImmutableSortedDictionaryProperty { get; set; } From 2d1b90fbdbc2ef4a5f1b195850d9cf54e89566c0 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 12:32:57 +0800 Subject: [PATCH 107/193] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9A=84=E5=B1=9E=E6=80=A7=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/PropertyTypeTests.cs | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs index 346c1029..47a2fab1 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PropertyTypeTests.cs @@ -5,6 +5,7 @@ using System.Linq; using DotNetCampus.Cli.Compiler; using Microsoft.VisualStudio.TestTools.UnitTesting; + // ReSharper disable InconsistentNaming namespace DotNetCampus.Cli.Tests.ParsingStyles; @@ -16,8 +17,6 @@ public class PropertyTypeTests public void SupportManyTypes() { // Arrange - // "--boolean-property", "true", - // "--immutable-array-property", "a,b,c", string[] args = [ "--boolean-property", "true", @@ -35,6 +34,7 @@ public void SupportManyTypes() "--char-property", "a", "--string-property", "value", "--array-property", "a,b,c", + "--list-property", "a,b,c", "--collection-property", "a,b,c", "--read-only-collection-property", "a,b,c", "--hash-set-property", "a,b,c", @@ -51,19 +51,19 @@ public void SupportManyTypes() "--iset-property", "a,b,c", "--iimmutable-list-property", "a,b,c", "--iimmutable-set-property", "a,b,c", - "--key-value-pair-property", "key:value", - "--dictionary-property", "key:value,key2:value2", - "--immutable-dictionary-property", "key:value,key2:value2", - "--sorted-dictionary-property", "key:value,key2:value2", - "--sorted-list-property", "key:value,key2:value2", - "--immutable-sorted-dictionary-property", "key:value,key2:value2", - "--idictionary-property", "key:value,key2:value2", - "--iread-only-dictionary-property", "key:value,key2:value2", + "--key-value-pair-property", "key=value", + "--dictionary-property", "key=value,key2=value2", + "--immutable-dictionary-property", "key=value,key2=value2", + "--sorted-dictionary-property", "key=value,key2=value2", + "--immutable-sorted-dictionary-property", "key=value,key2=value2", + "--idictionary-property", "key=value,key2=value2", + "--iread-only-dictionary-property", "key=value,key2=value2", + "--enum-property", "ValueB", ]; var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); // Act - var options = commandLine.As(); + var options = commandLine.As(); // Assert Assert.IsNotNull(options); @@ -75,18 +75,19 @@ public void SupportManyTypes() Assert.AreEqual(1.1f, options.SingleProperty); Assert.AreEqual(1, options.Int32Property); Assert.AreEqual((uint)1, options.UInt32Property); - Assert.AreEqual((long)1, options.Int64Property); + Assert.AreEqual(1, options.Int64Property); Assert.AreEqual((ulong)1, options.UInt64Property); Assert.AreEqual((short)1, options.Int16Property); Assert.AreEqual((ushort)1, options.UInt16Property); Assert.AreEqual('a', options.CharProperty); Assert.AreEqual("value", options.StringProperty); CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ArrayProperty); - CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.CollectionProperty!); - CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.ReadOnlyCollectionProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ListProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.CollectionProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ReadOnlyCollectionProperty!); CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.HashSetProperty!.ToArray()); CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ImmutableArrayProperty.ToArray()); - CollectionAssert.AreEqual(new[] { "a", "b", "c" }, (ICollection)options.ImmutableListProperty!); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, options.ImmutableListProperty!); CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.ImmutableHashSetProperty!.ToArray()); CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.SortedSetProperty!.ToArray()); CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, options.ImmutableSortedSetProperty!.ToArray()); @@ -103,10 +104,36 @@ public void SupportManyTypes() { ["key"] = "value", ["key2"] = "value2", - }, (ICollection)options.DictionaryProperty!); + }, options.DictionaryProperty!); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, options.ImmutableDictionaryProperty!); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, options.SortedDictionaryProperty!); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, options.ImmutableSortedDictionaryProperty!); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, (ICollection)options.IDictionaryProperty!); + CollectionAssert.AreEquivalent(new Dictionary + { + ["key"] = "value", + ["key2"] = "value2", + }, (ICollection)options.IReadOnlyDictionaryProperty!); + Assert.AreEqual(TestEnum.ValueB, options.EnumProperty); } - public record TestOptions + public record AllInOneTestOptions { [Option] public bool? BooleanProperty { get; set; } @@ -153,6 +180,9 @@ public record TestOptions [Option] public string[]? ArrayProperty { get; set; } + [Option] + public List? ListProperty { get; set; } + [Option] public Collection? CollectionProperty { get; set; } @@ -221,5 +251,14 @@ public record TestOptions [Option] public IReadOnlyDictionary? IReadOnlyDictionaryProperty { get; set; } + + [Option] + public TestEnum? EnumProperty { get; set; } + } + + public enum TestEnum + { + ValueA, + ValueB, } } From b3adf5439490df75298643b2e5b92f4718da977b Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 12:56:49 +0800 Subject: [PATCH 108/193] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E7=9A=84=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/PositionalArgumentTests.cs | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs index ee9bfe21..efde0cbd 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs @@ -110,11 +110,6 @@ public void DoesNotSupportPostPositionalArguments(string[] args, TestCommandLine Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); } - // [TestMethod] - public void MatchPositionalArgumentRange(string[] args, TestCommandLineStyle style) - { - } - [TestMethod] [DataRow(new[] { "-o", "true", "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] -o true value")] public void DoesNotMatchPositionalArgumentRange_Boolean(string[] args, TestCommandLineStyle style) @@ -145,6 +140,29 @@ public void DoesNotMatchPositionalArgumentRange_Collection(string[] args, TestCo Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); } + [TestMethod] + [DataRow(new[] { "a", "b", "c", "d", "e", "f" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] a b c d e f")] + [DataRow(new[] { "-o", "value", "a", "b", "c", "d", "e", "f" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value a b c d e f")] + [DataRow(new[] { "a", "b", "c", "d", "e", "f", "-o", "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] a b c d e f -o value")] + [DataRow(new[] { "-o", "value", "a", "b", "c", "d", "e", "f", "--" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value a b c d e f --")] + [DataRow(new[] { "a", "b", "c", "d", "e", "f", "-o", "value", "--" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] a b c d e f -o value --")] + [DataRow(new[] { "-o", "value", "--", "a", "b", "c", "d", "e", "f" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] -o value -- a b c d e f")] + [DataRow(new[] { "a", "b", "-o", "value", "c", "d", "e", "f" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] a b -o value c d e f")] + [DataRow(new[] { "a", "b", "c", "-o", "value", "d", "e", "f" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] a b c -o value d e f")] + public void MatchPositionalArgumentRange(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + CollectionAssert.AreEqual(new[] { "a", "b" }, (ICollection)options.Value0!); + Assert.AreEqual("c", options.Value1); + CollectionAssert.AreEqual(new[] { "d", "e", "f" }, (ICollection)options.Value2!); + } + public record TestOptions { [Option('o', "option")] @@ -174,13 +192,16 @@ public record CollectionTestOptions public record MultiplePositionArgumentsOptions { + [Option('o', "option")] + public string? Option { get; set; } + [Value(0, 2)] public IReadOnlyList? Value0 { get; set; } - [Value(1)] + [Value(2)] public string? Value1 { get; set; } - [Value(2, int.MaxValue)] + [Value(3, int.MaxValue)] public IReadOnlyList? Value2 { get; set; } } } From 922bb83651e14b1fb12843736f1275ee60517d03 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 12:59:54 +0800 Subject: [PATCH 109/193] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20URL=20=E8=BD=AC?= =?UTF-8?q?=E4=B9=89=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/UrlCommandTests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/UrlCommandTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/UrlCommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/UrlCommandTests.cs new file mode 100644 index 00000000..a03564e4 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/UrlCommandTests.cs @@ -0,0 +1,55 @@ +using DotNetCampus.Cli.Compiler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class UrlCommandTests +{ + [TestMethod] + // 空格 + [DataRow(new[] { "test://?option=value%20with%20space" }, "value with space", DisplayName = "[Uri] test://?option=value%20with%20space")] + // 特殊字符(# & % 等) + [DataRow(new[] { "test://?option=special%23chars%26more%25" }, "special#chars&more%", DisplayName = "[Uri] test://?option=special%23chars%26more%25")] + // 保留字符(/ ? : @ 等) + [DataRow(new[] { "test://?option=reserved%2Fchars%3F%3A%40" }, "reserved/chars?:@", DisplayName = "[Uri] test://?option=reserved%2Fchars%3F%3A%40")] + // 中文和其他非 ASCII 字符 + [DataRow(new[] { "test://?option=%E4%B8%AD%E6%96%87" }, "中文", DisplayName = "[Uri] test://?option=%E4%B8%AD%E6%96%87")] + // emoji 字符 + [DataRow(new[] { "test://?option=%F0%9F%98%81" }, "😁", DisplayName = "[Uri] test://?option=%F0%9F%98%81")] + public void Escape(string[] args, string expectedValue) + { + // Arrange + var commandLine = CommandLine.Parse(args, TestCommandLineStyle.Url.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual(expectedValue, options.Option); + } + + [TestMethod] + [DataRow(new[] { "test://#anchor" }, "anchor", DisplayName = "[Uri] test://?option=value#anchor")] + [DataRow(new[] { "test://?option=value#anchor" }, "anchor", DisplayName = "[Uri] test://?option=value#anchor")] + public void Fragment(string[] args, string expectedValue) + { + // Arrange + var commandLine = CommandLine.Parse(args, TestCommandLineStyle.Url.ToParsingOptions()); + + // Act + var options = commandLine.As(); + + // Assert + Assert.AreEqual(expectedValue, options.Fragment); + } + + public record TestOptions + { + [Option('o', "option")] + public string? Option { get; set; } + + [Option("fragment")] + public string? Fragment { get; set; } + } +} From 4cb8674c26adaa1e390f210c299436e37e6155d2 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 13:00:04 +0800 Subject: [PATCH 110/193] =?UTF-8?q?URL=20=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=B7=B2=E8=BF=81=E7=A7=BB=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UrlCommandLineParserTests.cs | 712 ------------------ 1 file changed, 712 deletions(-) delete mode 100644 tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs deleted file mode 100644 index 58bf82ca..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/UrlCommandLineParserTests.cs +++ /dev/null @@ -1,712 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试 URL 风格命令行参数是否正确被解析。 -/// 注意:URL风格参数通常由Web浏览器或其他应用程序传入,而不是用户直接在终端输入。 -/// 因此URL风格参数通常只有一个完整的URL参数,而不是像其他风格那样有多个参数。 -/// -[TestClass] -public class UrlCommandLineParserTests -{ - private CommandLineParsingOptions Scheme { get; } = new CommandLineParsingOptions { SchemeNames = ["myapp"] }; - - #region 1. 基本URL解析测试 - - [TestMethod("1.1. 完整URL格式解析(含scheme、path、query参数)")] - public void CompleteUrl_WithSchemePathAndQuery_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://documents/open?readOnly=true&highlight=yes"]; - string? path = null; - bool? readOnly = false; - string? highlight = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - path = o.Path; - readOnly = o.ReadOnly; - highlight = o.Highlight; - }) - .Run(); - - // Assert - Assert.AreEqual("open", path); - Assert.IsTrue(readOnly); - Assert.AreEqual("yes", highlight); - } - - [Ignore("虽然正常解析时,这种Scheme不匹配应该抛异常;但我们是主命令行程序,兼容被 Web 调用;所以这种情况代码都进不来。")] - [TestMethod("1.2. 指定SchemeNames时正确匹配scheme")] - public void SchemeNames_SpecifiedAndMatched_ParsedCorrectly() - { - // Arrange - string[] args = ["sample://action?param=value"]; - string? action = null; - string? param = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - action = o.Action; - param = o.Param; - }) - .Run(); - - // Assert - Assert.AreEqual("action", action); - Assert.AreEqual("value", param); - } - - [TestMethod("1.3. 不在SchemeNames列表中的scheme不被识别为URL")] - public void SchemeNames_NotMatched_NotParsedAsUrl() - { - // Arrange - string[] args = ["unknown://path?param=value"]; - string? value = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("unknown://path?param=value", value); // 作为普通位置参数处理 - } - - [TestMethod("1.4. 大小写不敏感的scheme匹配")] - public void SchemeNames_CaseInsensitive_MatchesCorrectly() - { - // Arrange - string[] args = ["MYAPP://path?param=value"]; - string? path = null; - string? param = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - path = o.Path; - param = o.Param; - }) - .Run(); - - // Assert - Assert.AreEqual("path", path); - Assert.AreEqual("value", param); - } - - #endregion - - #region 2. 查询参数(QueryString)解析测试 - - [TestMethod("2.1. 基本键值对参数解析")] - public void BasicQueryParam_KeyValuePair_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?name=value"]; - string? name = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => name = o.Name) - .Run(); - - // Assert - Assert.AreEqual("value", name); - } - - [TestMethod("2.2. 多参数解析")] - public void MultipleQueryParams_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?name=John&age=25&location=Beijing"]; - string? name = null; - int? age = null; - string? location = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - name = o.Name; - age = o.Age; - location = o.Location; - }) - .Run(); - - // Assert - Assert.AreEqual("John", name); - Assert.AreEqual(25, age); - Assert.AreEqual("Beijing", location); - } - - [TestMethod("2.3. 无值参数解析为布尔true")] - public void QueryParamWithoutValue_ParsedAsTrue() - { - // Arrange - string[] args = ["myapp://path?debug&verbose"]; - bool? debug = false; - bool? verbose = false; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - debug = o.Debug; - verbose = o.Verbose; - }) - .Run(); - - // Assert - Assert.IsTrue(debug); - Assert.IsTrue(verbose); - } - - [TestMethod("2.4. 空值参数解析为空字符串")] - public void QueryParamWithEmptyValue_ParsedAsEmptyString() - { - // Arrange - string[] args = ["myapp://path?name=&comment="]; - string? name = "default"; - string? comment = "default"; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - name = o.Name; - comment = o.Comment; - }) - .Run(); - - // Assert - Assert.AreEqual("", name); - Assert.AreEqual("", comment); - } - - [TestMethod("2.5. 同名参数多次出现(数组)解析")] - public void DuplicateQueryParams_ParsedAsArray() - { - // Arrange - string[] args = ["myapp://path?tags=csharp&tags=dotnet&tags=cli"]; - string[]? tags = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => tags = o.Tags) - .Run(); - - // Assert - Assert.IsNotNull(tags); - Assert.AreEqual(3, tags.Length); - CollectionAssert.AreEqual(new[] { "csharp", "dotnet", "cli" }, tags); - } - - #endregion - - #region 3. 类型转换测试 - - [TestMethod("3.1. 整数类型转换")] - public void QueryParamIntegerType_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?id=42&count=100"]; - int? id = null; - int? count = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - id = o.Id; - count = o.Count; - }) - .Run(); - - // Assert - Assert.AreEqual(42, id); - Assert.AreEqual(100, count); - } - - [TestMethod("3.2. 布尔类型转换")] - public void QueryParamBooleanType_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?enabled=true&disabled=false&flag"]; - bool? enabled = null; - bool? disabled = null; - bool? flag = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - enabled = o.Enabled; - disabled = o.Disabled; - flag = o.Flag; - }) - .Run(); - - // Assert - Assert.IsTrue(enabled); - Assert.IsFalse(disabled); - Assert.IsTrue(flag); - } - - [TestMethod("3.3. 枚举类型转换")] - public void QueryParamEnumType_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?logLevel=Warning&style=gnu"]; - LogLevel? logLevel = null; - CommandLineStyle? style = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - logLevel = o.LogLevel; - style = o.Style; - }) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); - Assert.AreEqual(CommandLineStyle.Gnu, style); - } - - [TestMethod("3.4. 数组/列表类型转换")] - public void QueryParamCollectionType_ParsedCorrectly() - { - // Arrange - string[] args = ["myapp://path?ids=1&ids=2&ids=3&names=Alice&names=Bob"]; - string[]? ids = null; - List? names = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - ids = o.Ids; - names = o.Names?.ToList(); - }) - .Run(); - - // Assert - Assert.IsNotNull(ids); - Assert.AreEqual(3, ids.Length); - CollectionAssert.AreEqual(new[] { "1", "2", "3" }, ids); - - Assert.IsNotNull(names); - Assert.AreEqual(2, names.Count); - CollectionAssert.AreEqual(new[] { "Alice", "Bob" }, names); - } - - #endregion - - #region 4. URL编码解析测试 - - [TestMethod("4.1. 基本URL编码解析(空格等)")] - public void UrlEncodedSpaces_DecodedCorrectly() - { - // Arrange - string[] args = ["myapp://path?query=hello%20world&path=my%20documents"]; - string? query = null; - string? path = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - query = o.Query; - path = o.Path; - }) - .Run(); - - // Assert - Assert.AreEqual("hello world", query); - Assert.AreEqual("my documents", path); - } - - [TestMethod("4.2. 特殊字符编码解析(#、&、%等)")] - public void UrlEncodedSpecialChars_DecodedCorrectly() - { - // Arrange - string[] args = ["myapp://path?special=hash%23ampersand%26percent%25"]; - string? special = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => special = o.Special) - .Run(); - - // Assert - Assert.AreEqual("hash#ampersand&percent%", special); - } - - [TestMethod("4.3. 中文和非ASCII字符编码解析")] - public void UrlEncodedNonAsciiChars_DecodedCorrectly() - { - // Arrange - string[] args = ["myapp://path?chinese=%E4%BD%A0%E5%A5%BD&emoji=%F0%9F%98%80"]; - string? chinese = null; - string? emoji = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - chinese = o.Chinese; - emoji = o.Emoji; - }) - .Run(); - - // Assert - Assert.AreEqual("你好", chinese); - Assert.AreEqual("😀", emoji); - } - - #endregion - - #region 5. 路径解析测试 - - [TestMethod("5.1. 路径部分作为位置参数")] - public void PathPart_ParsedAsPositionalValue() - { - // Arrange - string[] args = ["myapp://documents/reports/annual"]; - string[]? paths = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => paths = o.Paths) - .Run(); - - // Assert - CollectionAssert.AreEqual(new[] { "documents", "reports", "annual" }, paths); - } - - [TestMethod("5.2. 路径首部分作为命令名称,其余作为位置参数")] - public void FirstPathSegmentAsCommand_RemainingAsPositional() - { - // Arrange - string[] args = ["myapp://open/document.txt?readOnly=true"]; - string? filePath = null; - bool? readOnly = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - filePath = o.FilePath; - readOnly = o.ReadOnly; - }) - .Run(); - - // Assert - Assert.AreEqual("document.txt", filePath); - Assert.IsTrue(readOnly); - } - - #endregion - - #region 6. 边界情况测试 - - [TestMethod("6.1. 空参数列表")] - public void EmptyArgs_ProcessedGracefully() - { - // Arrange - string[] args = []; - string? path = "default"; - bool handlerCalled = false; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - handlerCalled = true; - path = o.Path; - }) - .Run(); - - // Assert - Assert.IsTrue(handlerCalled); - Assert.AreEqual("default-path", path); // 使用默认值 - } - - [Ignore("虽然正常解析时,这种格式应该抛异常;但我们是主命令行程序,兼容被 Web 调用;所以这种情况代码都进不来。")] - [TestMethod("6.2. 畸形URL格式")] - public void MalformedUrl_ThrowsException() - { - // Arrange - string[] args = ["myapp:/path?invalid-format"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Scheme) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.3. 重复的查询参数名")] - public void DuplicateQueryParamName_LastOneWins() - { - // Arrange - string[] args = ["myapp://path?name=first&name=second&name=last"]; - string? name = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => name = o.Name) - .Run(); - - // Assert - Assert.AreEqual("last", name); // 最后一个值被使用 - } - - [TestMethod("6.4. 特殊URL格式(片段标识符等)")] - public void SpecialUrlFormats_ParsedAppropriately() - { - // Arrange - string[] args = ["myapp://path?param=value#section"]; - string? param = null; - string? fragment = null; - - // Act - CommandLine.Parse(args, Scheme) - .AddHandler(o => - { - param = o.Param; - fragment = o.Fragment; - }) - .Run(); - - // Assert - Assert.AreEqual("value", param); - Assert.AreEqual("section", fragment); // 片段标识符被正确处理 - } - - #endregion -} - -#region 测试用数据模型 - -internal record Url01_BasicUrlOptions -{ - [Value(Length = int.MaxValue)] - public string Path { get; init; } = string.Empty; - - [Option("readOnly")] - public bool ReadOnly { get; init; } - - [Option] - public string Highlight { get; init; } = string.Empty; -} - -internal record Url02_SchemeOptions -{ - [Value] - public string Action { get; init; } = string.Empty; - - [Option] - public string Param { get; init; } = string.Empty; -} - -internal record Url03_PositionalValueOptions -{ - [Value] - public string Value { get; init; } = string.Empty; -} - -internal record Url04_CaseInsensitiveSchemeOptions -{ - [Value] - public string Path { get; init; } = string.Empty; - - [Option] - public string Param { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url05_BasicQueryParamOptions -{ - [Option] - public string Name { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url06_MultipleQueryParamOptions -{ - [Option] - public string Name { get; init; } = string.Empty; - - [Option] - public int Age { get; init; } - - [Option] - public string Location { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url07_BooleanQueryParamOptions -{ - [Option] - public bool Debug { get; init; } - - [Option] - public bool Verbose { get; init; } -} - -[Command("path")] -internal record Url08_EmptyValueQueryParamOptions -{ - [Option] - public string Name { get; init; } = string.Empty; - - [Option] - public string Comment { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url09_ArrayQueryParamOptions -{ - [Option] - public string[] Tags { get; init; } = []; -} - -[Command("path")] -internal record Url10_IntegerTypeOptions -{ - [Option] - public int Id { get; init; } - - [Option] - public int Count { get; init; } -} - -[Command("path")] -internal record Url11_BooleanTypeOptions -{ - [Option] - public bool Enabled { get; init; } - - [Option] - public bool Disabled { get; init; } - - [Option] - public bool Flag { get; init; } -} - -[Command("path")] -internal record Url12_EnumTypeOptions -{ - /// - /// 当前项目中的枚举。(源生成器应该要能正确识别。) - /// - [Option] - public LogLevel LogLevel { get; init; } - - /// - /// 引用项目中的枚举。(源生成器应该要能正确识别。) - /// - [Option] - public CommandLineStyle Style { get; init; } -} - -[Command("path")] -internal record Url13_CollectionTypeOptions -{ - [Option] - public string[] Ids { get; init; } = []; - - [Option] - public IReadOnlyList Names { get; init; } = []; -} - -[Command("path")] -internal record Url14_UrlEncodedOptions -{ - [Option] - public string Query { get; init; } = string.Empty; - - [Option] - public string Path { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url15_SpecialCharsOptions -{ - [Option] - public string Special { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url16_NonAsciiOptions -{ - [Option] - public string Chinese { get; init; } = string.Empty; - - [Option] - public string Emoji { get; init; } = string.Empty; -} - -internal record Url17_PathAsPositionalOptions -{ - [Value(Length = int.MaxValue)] - public required string[] Paths { get; init; } -} - -[Command("open")] -internal record Url18_CommandPathOptions -{ - [Value(0)] - public string FilePath { get; init; } = string.Empty; - - [Option] - public bool ReadOnly { get; init; } -} - -internal record Url19_DefaultValueOptions -{ - [Value] - public string Path { get; set; } = "default-path"; -} - -internal record Url20_MalformedUrlOptions -{ - [Option] - public string Value { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url21_DuplicateParamOptions -{ - [Option] - public string Name { get; init; } = string.Empty; -} - -[Command("path")] -internal record Url22_FragmentOptions -{ - [Option] - public string Param { get; init; } = string.Empty; - - [Option("fragment")] - public string Fragment { get; init; } = string.Empty; -} - -#endregion From 84d4fc742f02a8dd689c33df867e5621c00b3ced Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 13:02:23 +0800 Subject: [PATCH 111/193] =?UTF-8?q?=E5=81=87=E8=A3=85=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=AE=8C=E4=B9=8B=E5=89=8D=E7=9A=84=E6=B5=8B=E8=AF=95=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DotNetCampus.CommandLine.Sample.csproj | 6 - .../Fakes/Options.cs | 0 .../Fakes/OptionsParser.cs | 0 .../Fakes/VerbOptions.cs | 0 ...cs => CommandLineStyleMagicNumberTests.cs} | 2 +- .../DotNetCommandLineParserTests.cs | 1161 ----------------- .../Fakes/AmbiguousOptions.cs | 31 - .../Fakes/AmbiguousOptionsParser.cs | 34 - .../Fakes/CollectionOptions.cs | 23 - .../Fakes/CommandLineArgs.cs | 92 -- .../Fakes/DefaultCommandHandler.cs | 29 - .../Fakes/DictionaryOptions.cs | 19 - .../Fakes/FakeCommandHandler.cs | 30 - .../Fakes/FakeCommandOptions.cs | 18 - .../Fakes/PrimaryOptions.cs | 36 - .../Fakes/RuntimeImmutableOptions.cs | 73 -- .../Fakes/RuntimeOptions.cs | 52 - .../Fakes/UnlimitedValueOptions.cs | 16 - .../Fakes/ValueOptions.cs | 19 - .../FlexibleCommandLineParserTests.cs | 998 -------------- .../GnuCommandLineParserTests.cs | 1120 ---------------- .../LogLevel.cs | 10 - .../NamingConventionTests.cs | 718 ---------- .../PosixCommandLineParserTests.cs | 466 ------- .../PowerShellCommandLineParserTests.cs | 663 ---------- .../SubcommandTests.cs | 900 ------------- 26 files changed, 1 insertion(+), 6515 deletions(-) rename {tests/DotNetCampus.CommandLine.Tests => samples/DotNetCampus.CommandLine.Sample}/Fakes/Options.cs (100%) rename {tests/DotNetCampus.CommandLine.Tests => samples/DotNetCampus.CommandLine.Sample}/Fakes/OptionsParser.cs (100%) rename {tests/DotNetCampus.CommandLine.Tests => samples/DotNetCampus.CommandLine.Sample}/Fakes/VerbOptions.cs (100%) rename tests/DotNetCampus.CommandLine.Tests/{CommandLineParsingOptionsTests.cs => CommandLineStyleMagicNumberTests.cs} (86%) delete mode 100644 tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptionsParser.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/CollectionOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/DefaultCommandHandler.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/DictionaryOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandHandler.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/PrimaryOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeImmutableOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/UnlimitedValueOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Fakes/ValueOptions.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/LogLevel.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs diff --git a/samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj b/samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj index 0ec53685..ca8d8431 100644 --- a/samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj +++ b/samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj @@ -14,12 +14,6 @@ - - - - - - ..\..\tests\dotnetCampus.CommandLine.Performance\dotnetCampus.CommandLine.Legacy.dll diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs b/samples/DotNetCampus.CommandLine.Sample/Fakes/Options.cs similarity index 100% rename from tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs rename to samples/DotNetCampus.CommandLine.Sample/Fakes/Options.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/OptionsParser.cs b/samples/DotNetCampus.CommandLine.Sample/Fakes/OptionsParser.cs similarity index 100% rename from tests/DotNetCampus.CommandLine.Tests/Fakes/OptionsParser.cs rename to samples/DotNetCampus.CommandLine.Sample/Fakes/OptionsParser.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/VerbOptions.cs b/samples/DotNetCampus.CommandLine.Sample/Fakes/VerbOptions.cs similarity index 100% rename from tests/DotNetCampus.CommandLine.Tests/Fakes/VerbOptions.cs rename to samples/DotNetCampus.CommandLine.Sample/Fakes/VerbOptions.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs similarity index 86% rename from tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs rename to tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs index 0bd2db77..bd1693f8 100644 --- a/tests/DotNetCampus.CommandLine.Tests/CommandLineParsingOptionsTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs @@ -3,7 +3,7 @@ namespace DotNetCampus.Cli.Tests; [TestClass] -public class CommandLineParsingOptionsTests +public class CommandLineStyleMagicNumberTests { #if DEBUG [TestMethod("魔法数字必须严格和实际样式匹配")] diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs deleted file mode 100644 index 0bbd9b81..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCommandLineParserTests.cs +++ /dev/null @@ -1,1161 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试 DotNet 风格命令行参数是否正确被解析。 -/// -[TestClass] -public class DotNetCommandLineParserTests -{ - private CommandLineParsingOptions DotNet { get; } = CommandLineParsingOptions.DotNet; - - #region 1. 选项识别与解析 - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.1. 短选项冒号形式 (-option:value),字符串类型,可正常赋值。")] - public void ShortOption_WithColon_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-value:test"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.2. 长选项冒号形式 (--option:value),字符串类型,可正常赋值。")] - public void LongOption_WithColon_StringType_ValueAssigned() - { - // Arrange - string[] args = ["--value:test"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.3. 斜杠前缀形式 (/option:value),字符串类型,可正常赋值。")] - public void SlashPrefix_WithColon_StringType_ValueAssigned() - { - // Arrange - string[] args = ["/value:test"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.4. 多个选项混合使用,全部正确解析。")] - public void MixedOptions_MultipleParsed_AllAssigned() - { - // Arrange - string[] args = ["-number:42", "--text:hello", "/flag:true"]; - int? number = null; - string? text = null; - bool? flag = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - number = o.Number; - text = o.Text; - flag = o.Flag; - }) - .Run(); - - // Assert - Assert.AreEqual(42, number); - Assert.AreEqual("hello", text); - Assert.IsTrue(flag); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.5. PascalCase命名风格选项,可正常解析。")] - public void PascalCaseOption_Parsed_ValueAssigned() - { - // Arrange - string[] args = ["-PascalCase:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.PascalCase) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("1.6. camelCase命名风格选项,可正常解析。")] - public void CamelCaseOption_Parsed_ValueAssigned() - { - // Arrange - string[] args = ["--camelCase:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.CamelCase) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("1.7. kebab-case命名风格选项,可正常解析。")] - public void KebabCaseOption_Parsed_ValueAssigned() - { - // Arrange - string[] args = ["--kebab-case:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.KebabCase) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.8. 不同前缀的PascalCase风格选项,可正常解析。")] - public void MixedPrefixWithPascalCase_Parsed_ValueAssigned() - { - // Arrange - string[] args = ["-Option1:value1", "--Option2:value2", "/Option3:value3"]; - string? option1 = null; - string? option2 = null; - string? option3 = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - option1 = o.Option1; - option2 = o.Option2; - option3 = o.Option3; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", option1); - Assert.AreEqual("value2", option2); - Assert.AreEqual("value3", option3); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("1.9. 单字符短选项,不同前缀,可正常解析。")] - public void SingleCharOptions_DifferentPrefixes_Parsed() - { - // Arrange - string[] args = ["-a:value1", "/b:value2"]; - string? optionA = null; - string? optionB = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - optionA = o.A; - optionB = o.B; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", optionA); - Assert.AreEqual("value2", optionB); - } - - #endregion - - #region 2. 类型转换 - - [TestMethod("2.1. 整数类型,赋值成功。")] - public void IntegerOption_ValueAssigned() - { - // Arrange - string[] args = ["--number:42"]; - int? number = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => number = o.Number) - .Run(); - - // Assert - Assert.AreEqual(42, number); - } - - [TestMethod("2.2. 布尔类型,不带值赋为true。")] - public void BooleanOption_NoValue_SetTrue() - { - // Arrange - string[] args = ["--flag"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsTrue(flag); - } - - [TestMethod("2.2.1. 布尔类型,带值true赋为true。")] - public void BooleanOption_TrueValue_SetTrue() - { - // Arrange - string[] args = ["--flag:true"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsTrue(flag); - } - - [TestMethod("2.2.2. 布尔类型,带值false赋为false。")] - public void BooleanOption_FalseValue_SetFalse() - { - // Arrange - string[] args = ["--flag:false"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsFalse(flag); - } - - [TestMethod("2.3. 枚举类型,赋值成功。")] - public void EnumOption_ValueAssigned() - { - // Arrange - string[] args = ["--log-level:Warning"]; - LogLevel? logLevel = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => logLevel = o.LogLevel) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); - } - - [TestMethod("2.4. 字符串数组,赋值成功。")] - public void StringArrayOption_ValueAssigned() - { - // Arrange - string[] args = ["--files:file1.txt", "--files:file2.txt", "--files:file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [TestMethod("2.4.1. 字符串数组,使用分号分隔多个值,赋值成功。")] - public void StringArrayOption_SemicolonSeparated_ValueAssigned() - { - // Arrange - string[] args = ["--files:file1.txt;file2.txt;file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [TestMethod("2.4.2. 字符串数组,使用逗号分隔多个值,赋值成功。")] - public void StringArrayOption_CommaSeparated_ValueAssigned() - { - // Arrange - string[] args = ["--files:file1.txt,file2.txt,file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.4.3. 字符串数组,包含带引号的值,赋值成功。")] - public void StringArrayOption_QuotedValues_ValueAssigned() - { - // Arrange - string[] args = ["--files:\"file with spaces.txt\"", "--files:normal.txt", "--files:\"another file.txt\""]; - string[]? files = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.4.4. 字符串数组,使用分号分隔的带引号值,赋值成功。")] - public void StringArrayOption_SemicolonSeparatedQuoted_ValueAssigned() - { - // Arrange - string[] args = ["--files:\"file with spaces.txt\";normal.txt;\"another file.txt\""]; - string[]? files = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); - } - - [TestMethod("2.5. 列表类型,赋值成功。")] - public void ListOption_ValueAssigned() - { - // Arrange - string[] args = ["--tags:tag1", "--tags:tag2", "--tags:tag3"]; - List? tags = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => tags = o.Tags.ToList()) - .Run(); - - // Assert - Assert.IsNotNull(tags); - Assert.AreEqual(3, tags.Count); - CollectionAssert.AreEqual(new[] { "tag1", "tag2", "tag3" }, tags); - } - - [TestMethod("2.6.1. 字典类型,多次传入相同选项,赋值成功。")] - public void DictionaryOption_MultipleEntries_ValueAssigned() - { - // Arrange - string[] args = ["--properties:key1=value1", "--properties:key2=value2", "--properties:key3=value3"]; - Dictionary? properties = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => properties = new Dictionary(o.Properties)) - .Run(); - - // Assert - Assert.IsNotNull(properties); - Assert.AreEqual(3, properties.Count); - Assert.AreEqual("value1", properties["key1"]); - Assert.AreEqual("value2", properties["key2"]); - Assert.AreEqual("value3", properties["key3"]); - } - - [TestMethod("2.6.2. 字典类型,单次传入多个键值对,赋值成功。")] - public void DictionaryOption_SingleEntryMultiplePairs_ValueAssigned() - { - // Arrange - string[] args = ["--properties:key1=value1;key2=value2;key3=value3"]; - Dictionary? properties = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => properties = new Dictionary(o.Properties)) - .Run(); - - // Assert - Assert.IsNotNull(properties); - Assert.AreEqual(3, properties.Count); - Assert.AreEqual("value1", properties["key1"]); - Assert.AreEqual("value2", properties["key2"]); - Assert.AreEqual("value3", properties["key3"]); - } - - [TestMethod("2.6.3. 字典类型,混合方式传入,赋值成功。")] - public void DictionaryOption_MixedWays_ValueAssigned() - { - // Arrange - string[] args = ["--properties:key1=value1;key2=value2", "--properties:key3=value3"]; - Dictionary? properties = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => properties = new Dictionary(o.Properties)) - .Run(); - - // Assert - Assert.IsNotNull(properties); - Assert.AreEqual(3, properties.Count); - Assert.AreEqual("value1", properties["key1"]); - Assert.AreEqual("value2", properties["key2"]); - Assert.AreEqual("value3", properties["key3"]); - } - - [TestMethod("2.6.4. 字典类型,键没有对应值,解析抛出异常。")] - public void DictionaryOption_KeyWithoutValue_ThrowsException() - { - // Arrange - string[] args = ["--properties:key1=value1;key2"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("2.6.5. 字典类型,键值对格式错误,解析抛出异常。")] - public void DictionaryOption_InvalidFormat_ThrowsException() - { - // Arrange - string[] args = ["--properties:key1:value1"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("2.6.6. 字典类型,重复的键,后者覆盖前者。")] - public void DictionaryOption_DuplicateKeys_LastOneWins() - { - // Arrange - string[] args = ["--properties:key1=value1", "--properties:key1=value2"]; - Dictionary? properties = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => properties = new Dictionary(o.Properties)) - .Run(); - - // Assert - Assert.IsNotNull(properties); - Assert.AreEqual(1, properties.Count); - Assert.AreEqual("value2", properties["key1"]); - } - - [TestMethod("2.6.7. 字典类型,空值场景,成功解析为空字符串。")] - public void DictionaryOption_EmptyValue_ParsedAsEmptyString() - { - // Arrange - string[] args = ["--properties:key1="]; - Dictionary? properties = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => properties = new Dictionary(o.Properties)) - .Run(); - - // Assert - Assert.IsNotNull(properties); - Assert.AreEqual(1, properties.Count); - Assert.AreEqual("", properties["key1"]); - } - - [TestMethod("2.7. 不可变集合类型,赋值成功。")] - public void ImmutableCollectionOption_ValueAssigned() - { - // Arrange - string[] args = ["--items:item1", "--items:item2", "--items:item3"]; - ImmutableArray? items = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => items = o.Items) - .Run(); - - // Assert - Assert.IsNotNull(items); - Assert.AreEqual(3, items.Value.Length); - Assert.AreEqual("item1", items.Value[0]); - Assert.AreEqual("item2", items.Value[1]); - Assert.AreEqual("item3", items.Value[2]); - } - - #endregion - - #region 3. 边界情况处理 - - [TestMethod("3.1. 缺失必需选项,抛出异常。")] - public void MissingRequiredOption_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("3.2. 无效格式选项,抛出异常。")] - public void InvalidOption_ThrowsException() - { - // Arrange - string[] args = ["---invalid:value"]; // 三个破折号是无效的 - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("3.3. 类型不匹配,抛出异常。")] - public void TypeMismatch_ThrowsException() - { - // Arrange - string[] args = ["--number:not-a-number"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.4. 大小写不敏感,识别正确。")] - public void CaseInsensitive_CorrectOptionParsed() - { - // Arrange - string[] args = ["--Ignore-Case:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.IgnoreCase) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - #endregion - - #region 4. 特殊特性 - - [TestMethod("4.1. 选项别名,识别正确。")] - public void OptionAliases_CorrectOptionParsed() - { - // Arrange - string[] args = ["--alt:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.OptionWithAlias) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("4.2. 终止选项解析符号,识别正确。")] - public void OptionTerminator_FollowingArgsAreValues() - { - // Arrange - string[] args = ["--option:value", "--", "--not-an-option", "-x"]; - string? option = null; - string[]? values = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - option = o.Option; - values = o.Values; - }) - .Run(); - - // Assert - Assert.AreEqual("value", option); - Assert.IsNotNull(values); - Assert.AreEqual(2, values.Length); - Assert.AreEqual("--not-an-option", values[0]); - Assert.AreEqual("-x", values[1]); - } - - #endregion - - #region 5. 位置参数处理 - - [TestMethod("5.1. 单个位置参数,赋值成功。")] - public void SinglePositionalValue_ValueAssigned() - { - // Arrange - string[] args = ["positional-value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("positional-value", value); - } - - [TestMethod("5.2. 多个位置参数,赋值成功。")] - public void MultiplePositionalValues_AllAssigned() - { - // Arrange - string[] args = ["value1", "value2", "value3"]; - string[]? values = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => values = o.Values) - .Run(); - - // Assert - Assert.IsNotNull(values); - Assert.AreEqual(3, values.Length); - CollectionAssert.AreEqual(new[] { "value1", "value2", "value3" }, values); - } - - [TestMethod("5.3. 位置参数与选项混合,识别正确。")] - public void MixedPositionalAndOptions_AllParsedCorrectly() - { - // Arrange - string[] args = ["value1", "--option:opt-val", "value2"]; - string? option = null; - string? value1 = null; - string? value2 = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - option = o.Option; - value1 = o.Value1; - value2 = o.Value2; - }) - .Run(); - - // Assert - Assert.AreEqual("opt-val", option); - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("5.4. 指定索引位置参数,识别正确。")] - public void IndexedPositionalValues_CorrectAssignment() - { - // Arrange - string[] args = ["first", "second", "third"]; - string? first = null; - string? third = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - first = o.First; - third = o.Third; - }) - .Run(); - - // Assert - Assert.AreEqual("first", first); - Assert.AreEqual("third", third); - } - - #endregion - - #region 6. Required 和 Nullable 组合测试 - - [TestMethod("6.1. Non-required, Non-nullable, 无CLI参数,使用默认值。")] - public void NonRequiredNonNullable_NoCli_UsesDefault() - { - // Arrange - string[] args = []; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual(null, value); // 使用初始化时的默认值 - } - - [TestMethod("6.2. Required, Non-nullable, 无CLI参数,抛出异常。")] - public void RequiredNonNullable_NoCli_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.3. Non-required, Nullable, 无CLI参数,赋默认值(null)。")] - public void NonRequiredNullable_NoCli_DefaultNull() - { - // Arrange - string[] args = []; - string? value = "not-null"; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.IsNull(value); - } - - [TestMethod("6.4. Required, Nullable, 无CLI参数,抛出异常。")] - public void RequiredNullable_NoCli_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, DotNet) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.5. 各种组合都提供CLI参数,全部赋值成功。")] - public void AllCombinations_WithCli_AllAssigned() - { - // Arrange - string[] args = - [ - "--req-non-null:value1", "--non-req-null:value2", - "--req-null:value3", "--non-req-non-null:value4" - ]; - string? reqNonNull = null; - string? nonReqNull = null; - string? reqNull = null; - string? nonReqNonNull = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => - { - reqNonNull = o.ReqNonNull; - nonReqNull = o.NonReqNull; - reqNull = o.ReqNull; - nonReqNonNull = o.NonReqNonNull; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", reqNonNull); - Assert.AreEqual("value2", nonReqNull); - Assert.AreEqual("value3", reqNull); - Assert.AreEqual("value4", nonReqNonNull); - } - - #endregion - - #region 7. 异步处理测试 - - [TestMethod("7.1. 异步处理方法,正确执行。")] - public async Task AsyncHandler_ExecutesCorrectly() - { - // Arrange - string[] args = ["--value:async-test"]; - string? value = null; - - // Act - await CommandLine.Parse(args, DotNet) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - value = o.Value; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("async-test", value); - } - - #endregion - - #region 8. DotNet特定风格测试 - - [TestMethod("8.1. DotNet风格,双破折号+PascalCase,可正常解析。")] - public void DotNetStyle_DoubleDashPascalCase_Parsed() - { - // Arrange - string[] args = ["--OptionName:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.OptionName) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("8.2. DotNet风格,单破折号+PascalCase,可正常解析。")] - public void DotNetStyle_SingleDashPascalCase_Parsed() - { - // Arrange - string[] args = ["-OptionName:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.OptionName) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("8.3. DotNet风格,斜杠+PascalCase,可正常解析。")] - public void DotNetStyle_SlashPascalCase_Parsed() - { - // Arrange - string[] args = ["/OptionName:value"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.OptionName) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("8.4. DotNet风格,支持两字符短选项,可正常解析。")] - public void DotNetStyle_TwoCharShortOption_Parsed() - { - // Arrange - string[] args = ["-tl:off"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.TerminalLogger) - .Run(); - - // Assert - Assert.AreEqual("off", value); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("8.5. DotNet风格,斜杠前缀两字符短选项,可正常解析。")] - public void DotNetStyle_SlashTwoCharOption_Parsed() - { - // Arrange - string[] args = ["/tl:off"]; - string? value = null; - - // Act - CommandLine.Parse(args, DotNet) - .AddHandler(o => value = o.TerminalLogger) - .Run(); - - // Assert - Assert.AreEqual("off", value); - } - - #endregion -} - -#region 测试用数据模型 - -internal record DotNet01_StringOptions -{ - [Option] - public required string Value { get; init; } -} - -internal record DotNet02_PascalCaseOptions -{ - [Option("PascalCase")] - public string PascalCase { get; init; } = string.Empty; - - [Option("camelCase")] - public string CamelCase { get; init; } = string.Empty; - - [Option("kebab-case")] - public string KebabCase { get; init; } = string.Empty; -} - -internal record DotNet03_MixedOptions -{ - [Option] - public int Number { get; init; } - - [Option] - public required string Text { get; init; } - - [Option] - public bool Flag { get; init; } -} - -internal record DotNet04_IntegerOptions -{ - [Option] - public int Number { get; init; } -} - -internal record DotNet05_BooleanOptions -{ - [Option] - public bool Flag { get; init; } -} - -internal record DotNet06_EnumOptions -{ - [Option("log-level")] - public LogLevel LogLevel { get; init; } -} - -internal record DotNet07_ArrayOptions -{ - [Option] - public string[] Files { get; init; } = []; -} - -internal record DotNet08_ListOptions -{ - [Option] - public IReadOnlyList Tags { get; init; } = []; -} - -internal record DotNet09_RequiredOptions -{ - [Option] - public required string RequiredValue { get; init; } -} - -internal record DotNet11_CaseInsensitiveOptions -{ - [Option("ignore-case")] - public string IgnoreCase { get; init; } = string.Empty; -} - -internal record DotNet12_AliasOptions -{ - [Option([], ["option-with-alias", "alt", "alternate"])] - public string OptionWithAlias { get; init; } = string.Empty; -} - -internal record DotNet14_TerminatorOptions -{ - [Option] - public string Option { get; init; } = string.Empty; - - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record DotNet15_SingleValueOptions -{ - [Value] - public string Value { get; init; } = string.Empty; -} - -internal record DotNet16_MultipleValueOptions -{ - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record DotNet17_MixedValueOptions -{ - [Value(0)] - public string Value1 { get; init; } = string.Empty; - - [Option] - public string Option { get; init; } = string.Empty; - - [Value(1)] - public string Value2 { get; init; } = string.Empty; -} - -internal record DotNet18_IndexedValueOptions -{ - [Value(0)] - public string First { get; init; } = string.Empty; - - [Value(2)] - public string Third { get; init; } = string.Empty; -} - -internal record DotNet19_RequiredNonNullableOption -{ - [Option] - public required string Value { get; init; } -} - -internal record DotNet20_NonRequiredNullableOption -{ - [Option] - public string? Value { get; init; } -} - -internal record DotNet21_RequiredNullableOption -{ - [Option] - public required string? Value { get; init; } -} - -internal record DotNet22_AllCombinationsOption -{ - [Option("req-non-null")] - public required string ReqNonNull { get; init; } - - [Option("non-req-null")] - public string? NonReqNull { get; init; } - - [Option("req-null")] - public required string? ReqNull { get; init; } - - [Option("non-req-non-null")] - public string NonReqNonNull { get; init; } = string.Empty; -} - -internal record DotNet23_DictionaryOptions -{ - [Option] - public IReadOnlyDictionary Properties { get; init; } = new Dictionary(); -} - -internal record DotNet24_ImmutableCollectionOptions -{ - [Option] - public ImmutableArray Items { get; init; } = ImmutableArray.Empty; -} - -internal record DotNet25_MixedPrefixOptions -{ - [Option("Option1")] - public string Option1 { get; init; } = string.Empty; - - [Option("Option2")] - public string Option2 { get; init; } = string.Empty; - - [Option("Option3")] - public string Option3 { get; init; } = string.Empty; -} - -internal record DotNet26_SingleCharOptions -{ - [Option("a")] - public string A { get; init; } = string.Empty; - - [Option("b")] - public string B { get; init; } = string.Empty; -} - -internal record DotNet27_NonRequiredNonNullableOption -{ - [Option] - public string Value { get; init; } = string.Empty; -} - -internal record DotNet28_DotNetSpecificOptions -{ - [Option("OptionName")] - public string OptionName { get; init; } = string.Empty; -} - -internal record DotNet29_TwoCharOptions -{ - [Option("tl", "terminal-logger")] - public string TerminalLogger { get; init; } = string.Empty; -} - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptions.cs deleted file mode 100644 index a0078893..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class AmbiguousOptions -{ - /// - /// 命令行中传入 --boolean 也可,传入 --boolean true 也可。 - /// - [Option("Boolean")] - public bool Boolean { get; set; } - - /// - /// 命令行中传入 --string-boolean true 也可,会使得值为 true。 - /// - [Option("StringBoolean")] - public string? StringBoolean { get; set; } - - /// - /// 命令行中传入 --string-array a 也可。 - /// - [Option("StringArray")] - public string? StringArray { get; set; } - - /// - /// 命令行中传入 --string-array a 也可,传入 --string-array a b 也可。 - /// - [Option("Array")] - public IReadOnlyList? Array { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptionsParser.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptionsParser.cs deleted file mode 100644 index faa7640b..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptionsParser.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using dotnetCampus.Cli; - -namespace DotNetCampus.Cli.Tests.Fakes -{ - public class AmbiguousOptionsParser : CommandLineOptionParser - { - public AmbiguousOptionsParser() - { - bool boolean = false; - string? stringBoolean = null; - string? stringArray = null; - IReadOnlyList? array = null; - - AddMatch("Boolean", value => boolean = value); - AddMatch("StringBoolean", value => stringBoolean = value); - AddMatch("StringArray", value => stringArray = value); - AddMatch("Array", value => array = value); - - SetResult(() => new AmbiguousOptions() - { - Boolean = boolean, - StringBoolean = stringBoolean, - StringArray = stringArray, - Array = array, - }); - } - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/CollectionOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/CollectionOptions.cs deleted file mode 100644 index 8390a3f2..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/CollectionOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class CollectionOptions -{ - [Option("ReadOnlyList")] - public IReadOnlyList? ReadOnlyList { get; set; } - - [Option("List")] - public IList? List { get; set; } - - [Option("Collection")] - public Collection? Collection { get; set; } - - [Option("Array")] - public string[]? Array { get; set; } - - [Option("Enumerable")] - public IEnumerable? Enumerable { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs deleted file mode 100644 index 63f89e69..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace DotNetCampus.Cli.Tests.Fakes; - -internal static class CommandLineArgs -{ - internal const string UrlProtocol = "walterlv"; - internal const string FileValue = @"C:\Users\lvyi\Desktop\文件.txt"; - internal const bool CloudValue = true; - internal const bool IwbValue = true; - internal const string ModeValue = "Display"; - internal const bool SilenceValue = true; - internal const string PlacementValue = "Outside"; - internal const string StartupSessionValue = "89EA9D26-6464-4E71-BD04-AA6516063D83"; - - internal static readonly string[] NoArgs = []; - - internal static readonly string[] DotNetStyleArgs = - { - FileValue, - "--cloud", - "--iwb", - "-m", - ModeValue, - "-s", - "-p", - PlacementValue, - "--startup-session", - StartupSessionValue, - }; - - internal static readonly string[] PowerShellStyleArgs = - { - FileValue, - "-Cloud", - "-Iwb", - "-m", - ModeValue, - "-s", - "-p", - PlacementValue, - "-StartupSession", - StartupSessionValue, - }; - - internal static readonly string[] CmdStyleArgs = - { - FileValue, - "/Cloud", - "/Iwb", - "/m", - ModeValue, - "/s", - "/p", - PlacementValue, - "/StartupSession", - StartupSessionValue, - }; - - internal static readonly string[] Cmd2StyleArgs = - { - FileValue, - "/Cloud", - "/Iwb", - $"/m:{ModeValue}", - "/s", - $"/p:{PlacementValue}", - $"/StartupSession:{StartupSessionValue}", - }; - - internal static readonly string[] GnuStyleArgs = - { - FileValue, - "--cloud", - "--iwb", - "-m", - ModeValue, - "-s", - "-p", - PlacementValue, - "--startup-session", - StartupSessionValue, - }; - - internal static readonly string[] UrlArgs = - { - @"walterlv://open/?file=C:\Users\lvyi\Desktop\%E6%96%87%E4%BB%B6.txt&cloud=true&iwb=true&mode=Display&silence=true&placement=Outside&startupSession=89EA9D26-6464-4E71-BD04-AA6516063D83", - }; - - internal static readonly string[] EditVerbArgs = - { - "Edit", "XXX", - }; -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/DefaultCommandHandler.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/DefaultCommandHandler.cs deleted file mode 100644 index 53cc18bd..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/DefaultCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class DefaultCommandHandler : ICommandHandler -{ - [Option("Fake")] - public string? Fake { get; init; } - - [Option("FakeProperty")] - public string? FakeProperty { get; init; } - - [Value] - public string? Argument { get; init; } - - public Func? Runner { get; set; } - - public Task RunAsync() - { - if (Runner is not { } runner) - { - throw new InvalidOperationException("No runner is set."); - } - - return Task.FromResult(runner()); - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/DictionaryOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/DictionaryOptions.cs deleted file mode 100644 index 578a13ac..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/DictionaryOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class DictionaryOptions -{ - [Option('a', "Aaa")] - public IReadOnlyDictionary? Aaa { get; set; } - - [Option('b', "Bbb")] - public IDictionary? Bbb { get; set; } - - [Option('c', "Ccc")] - public Dictionary? Ccc { get; set; } - - [Option('d', "Ddd")] - public KeyValuePair Ddd { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandHandler.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandHandler.cs deleted file mode 100644 index 2d25a42e..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -[Command("Fake")] -public class FakeCommandHandler : ICommandHandler -{ - [Option("Fake")] - public string? Fake { get; init; } - - [Option("FakeProperty")] - public string? FakeProperty { get; init; } - - [Value] - public string? Argument { get; init; } - - public Func? Runner { get; set; } - - public Task RunAsync() - { - if (Runner is not { } runner) - { - throw new InvalidOperationException("No runner is set."); - } - - return Task.FromResult(runner()); - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandOptions.cs deleted file mode 100644 index 88b7955b..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class FakeCommandOptions -{ - [Option("Fake")] - public string? Fake { get; init; } - - [Option("FakeProperty")] - public string? FakeProperty { get; init; } - - [Value] - public string? Argument { get; init; } - - public Func? Runner { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/PrimaryOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/PrimaryOptions.cs deleted file mode 100644 index d9ae4de1..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/PrimaryOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class PrimaryOptions -{ - [Option('a', "Byte")] - public byte Aaa { get; set; } - - [Option('b', "Int16")] - public short Bbb { get; set; } - - [Option('c', "UInt16")] - public ushort Ccc { get; set; } - - [Option('d', "Int32")] - public int Ddd { get; set; } - - [Option('e', "UInt32")] - public uint Eee { get; set; } - - [Option('f', "Int64")] - public long Fff { get; set; } - - [Option('g', "UInt64")] - public ulong Ggg { get; set; } - - [Option('h', "Single")] - public float Hhh { get; set; } - - [Option('i', "Double")] - public double Iii { get; set; } - - [Option('j', "Decimal")] - public decimal Jjj { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeImmutableOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeImmutableOptions.cs deleted file mode 100644 index 5af7c6de..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeImmutableOptions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.ComponentModel; - -namespace DotNetCampus.Cli.Tests.Fakes -{ - /// - /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 - /// - public class RuntimeImmutableOptions - { - /// - /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Value(0), Option('f', "File")] - public string FilePath { get; } - - /// - /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 - /// - [Option("Cloud"), DefaultValue(false)] - public bool IsFromCloud { get; } - - /// - /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('m', "Mode")] - public string StartupMode { get; } - - /// - /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 - /// - [Option('s', "Silence"), DefaultValue(false)] - public bool IsSilence { get; } - - /// - /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 - /// - [Option("Iwb"), DefaultValue(false)] - public bool IsIwb { get; } - - /// - /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('p', "Placement")] - public string Placement { get; } - - /// - /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option("StartupSession")] - public string StartupSession { get; } - - /// - /// 创建 类的新实例。 - /// - public RuntimeImmutableOptions( - string filePath, - bool isFromCloud, - string startupMode, - bool isSilence, - bool isIwb, - string placement, - string startupSession) - { - FilePath = filePath; - IsFromCloud = isFromCloud; - StartupMode = startupMode; - IsSilence = isSilence; - IsIwb = isIwb; - Placement = placement; - StartupSession = startupSession; - } - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeOptions.cs deleted file mode 100644 index acc615d6..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeOptions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.ComponentModel; - -namespace DotNetCampus.Cli.Tests.Fakes -{ - /// - /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 - /// - public class RuntimeOptions - { - /// - /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Value(0), Option('f', "File")] - public string? FilePath { get; set; } - - /// - /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 - /// - [Option("Cloud"), DefaultValue(false)] - public bool IsFromCloud { get; set; } - - /// - /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('m', "Mode")] - public string? StartupMode { get; set; } - - /// - /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 - /// - [Option('s', "Silence"), DefaultValue(false)] - public bool IsSilence { get; set; } - - /// - /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 - /// - [Option("Iwb"), DefaultValue(false)] - public bool IsIwb { get; set; } - - /// - /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option('p', "Placement")] - public string? Placement { get; set; } - - /// - /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 - /// - [Option("StartupSession")] - public string? StartupSession { get; set; } - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/UnlimitedValueOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/UnlimitedValueOptions.cs deleted file mode 100644 index bdfcb14d..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/UnlimitedValueOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class UnlimitedValueOptions -{ - [Option('s', nameof(Section))] - public string? Section { get; set; } - - [Value(0)] - public int Count { get; set; } - - [Value(1, int.MaxValue)] - public IEnumerable? Args { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/Fakes/ValueOptions.cs b/tests/DotNetCampus.CommandLine.Tests/Fakes/ValueOptions.cs deleted file mode 100644 index 5e758463..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Fakes/ValueOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using DotNetCampus.Cli.Compiler; - -namespace DotNetCampus.Cli.Tests.Fakes; - -public class ValueOptions -{ - [Option('f', nameof(Foo))] - public string? Foo { get; set; } - - [Value(0)] - public long LongValue { get; set; } - - [Value(1, 2)] - public IReadOnlyList? Values { get; set; } - - [Value(2)] - public int Int32Value { get; set; } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs deleted file mode 100644 index 1fe04d21..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/FlexibleCommandLineParserTests.cs +++ /dev/null @@ -1,998 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试 Flexible 风格命令行参数是否正确被解析。 -/// -[TestClass] -public class FlexibleCommandLineParserTests -{ - private CommandLineParsingOptions Flexible { get; } = CommandLineParsingOptions.Flexible; - - #region 1. 参数前缀支持多种形式 - - [TestMethod("1.1. 支持双破折线(--) + 字符串类型参数")] - public void DoubleHyphen_StringType_ValueAssigned() - { - // Arrange - string[] args = ["--value", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.2. 支持单破折线(-) + 字符串类型参数")] - public void SingleHyphen_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-value", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.3. 支持斜杠(/) + 字符串类型参数")] - public void Slash_StringType_ValueAssigned() - { - // Arrange - string[] args = ["/value", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - #endregion - - #region 2. 参数值分隔符兼容多种形式 - - [TestMethod("2.1. 支持空格作为分隔符")] - public void SpaceSeparator_ValueAssigned() - { - // Arrange - string[] args = ["--value", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("2.2. 支持等号(=)作为分隔符")] - public void EqualSeparator_ValueAssigned() - { - // Arrange - string[] args = ["--value=test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("2.3. 支持冒号(:)作为分隔符")] - public void ColonSeparator_ValueAssigned() - { - // Arrange - string[] args = ["--value:test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [Ignore("只有 GNU 风格支持。Flexible 包容万象,但包容不下这种偏门功能。")] - [TestMethod("2.4. 短选项支持无分隔符直接跟参数(GNU风格)")] - public void ShortOption_NoSeparator_ValueAssigned() - { - // Arrange - string[] args = ["-vtest"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.V) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("2.5. 短选项与分隔符混合使用")] - public void ShortOption_MixedSeparators_AllAssigned() - { - // Arrange - string[] args = ["-a", "value1", "-b=value2", "-c:value3"]; - string? valueA = null; - string? valueB = null; - string? valueC = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - valueA = o.A; - valueB = o.B; - valueC = o.C; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", valueA); - Assert.AreEqual("value2", valueB); - Assert.AreEqual("value3", valueC); - } - - #endregion - - #region 3. 参数命名风格兼容性 - - [TestMethod("3.1. 支持kebab-case命名风格")] - public void KebabCase_OptionName_ValueAssigned() - { - // Arrange - string[] args = ["--parameter-name", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.ParameterName) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("3.2. 支持PascalCase命名风格")] - public void PascalCase_OptionName_ValueAssigned() - { - // Arrange - string[] args = ["-ParameterName", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.ParameterName) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("3.3. 支持camelCase命名风格")] - public void CamelCase_OptionName_ValueAssigned() - { - // Arrange - string[] args = ["--parameterName", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.ParameterName) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - #endregion - - #region 4. 大小写不敏感测试 - - [TestMethod("4.1. 选项名大小写不敏感")] - public void CaseInsensitive_OptionName_ValueAssigned() - { - // Arrange - string[] args = ["--VALUE", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("4.2. 短选项大小写不敏感")] - public void CaseInsensitive_ShortOption_ValueAssigned() - { - // Arrange - string[] args = ["-V", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.V) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - #endregion - - #region 5. 短选项和长选项测试 - - [TestMethod("5.1. 短选项与长选项对应相同属性")] - public void ShortAndLongOption_SameProperty_ValueAssigned() - { - // Arrange - string[] args1 = ["--output", "file.txt"]; - string[] args2 = ["-o", "file.txt"]; - string? value1 = null; - string? value2 = null; - - // Act - CommandLine.Parse(args1, Flexible) - .AddHandler(o => value1 = o.Output) - .Run(); - - CommandLine.Parse(args2, Flexible) - .AddHandler(o => value2 = o.Output) - .Run(); - - // Assert - Assert.AreEqual("file.txt", value1); - Assert.AreEqual("file.txt", value2); - } - - [Ignore("自动形式经过讨论,不支持短选项组合;如果需要,请改用 GNU 风格。")] - [TestMethod("5.2. 支持有限的短选项组合")] - public void ShortOptionCombination_AllAssigned() - { - // Arrange - string[] args = ["-abc"]; - bool? flagA = null; - bool? flagB = null; - bool? flagC = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - flagA = o.A; - flagB = o.B; - flagC = o.C; - }) - .Run(); - - // Assert - Assert.IsTrue(flagA); - Assert.IsTrue(flagB); - Assert.IsTrue(flagC); - } - - #endregion - - #region 6. 布尔开关参数测试 - - [TestMethod("6.1. 不带值的布尔参数默认为true")] - public void BooleanFlag_NoValue_DefaultTrue() - { - // Arrange - string[] args = ["--flag"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsTrue(flag); - } - - [TestMethod("6.2. 布尔参数支持true/false值")] - public void BooleanFlag_ExplicitValue_Assigned() - { - // Arrange - string[] args = ["--flag=false"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsFalse(flag); - } - - [TestMethod("6.3. 布尔参数支持yes/no值")] - public void BooleanFlag_YesNoValue_Assigned() - { - // Arrange - string[] args = ["--flag=yes"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsTrue(flag); - } - - [TestMethod("6.4. 布尔参数支持on/off值")] - public void BooleanFlag_OnOffValue_Assigned() - { - // Arrange - string[] args = ["--flag=off"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsFalse(flag); - } - - [Ignore("否定形式(no-prefix)计划以后再实现。")] - [TestMethod("6.5. 支持否定形式(no-prefix)的布尔参数")] - public void BooleanFlag_NegatePrefix_Assigned() - { - // Arrange - string[] args = ["--no-feature"]; - bool? feature = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => feature = o.Feature) - .Run(); - - // Assert - Assert.IsFalse(feature); - } - - #endregion - - #region 7. 位置参数测试 - - [TestMethod("7.1. 单个位置参数解析正确")] - public void SinglePositionalParameter_ValueAssigned() - { - // Arrange - string[] args = ["positional-value"]; - string? value = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("positional-value", value); - } - - [TestMethod("7.2. 多个位置参数解析正确")] - public void MultiplePositionalParameters_AllAssigned() - { - // Arrange - string[] args = ["value1", "value2", "value3"]; - string[]? values = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => values = o.Values) - .Run(); - - // Assert - Assert.IsNotNull(values); - Assert.AreEqual(3, values.Length); - CollectionAssert.AreEqual(new[] { "value1", "value2", "value3" }, values); - } - - [TestMethod("7.3. 双破折号(--)后的内容作为位置参数")] - public void DoubleHyphen_TreatsFollowingAsValues() - { - // Arrange - string[] args = ["--option", "value", "--", "--not-an-option", "-x"]; - string? option = null; - string[]? values = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - option = o.Option; - values = o.Values; - }) - .Run(); - - // Assert - Assert.AreEqual("value", option); - Assert.IsNotNull(values); - Assert.AreEqual(2, values.Length); - Assert.AreEqual("--not-an-option", values[0]); - Assert.AreEqual("-x", values[1]); - } - - [TestMethod("7.4. 选项与位置参数混合使用")] - public void MixedOptionsAndValues_AllAssigned() - { - // Arrange - string[] args = ["value1", "--option=test", "value2"]; - string? option = null; - string? value1 = null; - string? value2 = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - option = o.Option; - value1 = o.Value1; - value2 = o.Value2; - }) - .Run(); - - // Assert - Assert.AreEqual("test", option); - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - } - - #endregion - - #region 8. 混合风格测试 - - [TestMethod("8.1. 混合使用多种风格的选项前缀")] - public void MixedPrefixStyles_AllAssigned() - { - // Arrange - string[] args = ["--option1", "value1", "-option2", "value2", "/option3", "value3"]; - string? value1 = null; - string? value2 = null; - string? value3 = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - value1 = o.Option1; - value2 = o.Option2; - value3 = o.Option3; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - Assert.AreEqual("value3", value3); - } - - [TestMethod("8.2. 混合使用多种分隔符")] - public void MixedSeparatorStyles_AllAssigned() - { - // Arrange - string[] args = ["--option1", "value1", "--option2=value2", "--option3:value3"]; - string? value1 = null; - string? value2 = null; - string? value3 = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - value1 = o.Option1; - value2 = o.Option2; - value3 = o.Option3; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - Assert.AreEqual("value3", value3); - } - - [TestMethod("8.3. 混合使用多种命名风格")] - public void MixedNamingStyles_AllAssigned() - { - // Arrange - string[] args = ["--kebab-case", "value1", "-PascalCase", "value2", "--camelCase", "value3"]; - string? kebabValue = null; - string? pascalValue = null; - string? camelValue = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - kebabValue = o.KebabCase; - pascalValue = o.PascalCase; - camelValue = o.CamelCase; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", kebabValue); - Assert.AreEqual("value2", pascalValue); - Assert.AreEqual("value3", camelValue); - } - - #endregion - - #region 9. 边界情况和错误处理 - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("9.1. 未知选项,抛出异常")] - public void UnknownOption_ThrowsException() - { - // Arrange - string[] args = ["--non-existent", "value"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .Run(); - }); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("9.2. 选项名称拼写错误时,抛出异常并提示近似选项")] - public void MisspelledOption_ThrowsExceptionWithHint() - { - // Arrange - string[] args = ["--valu", "test"]; // 应该是 --value - - // Act - var exception = Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .Run(); - }); - - // Assert - StringAssert.Contains(exception.Message, "value"); // 确保消息中包含近似的正确选项 - } - - [TestMethod("9.3. 类型不匹配时,抛出异常")] - public void TypeMismatch_ThrowsException() - { - // Arrange - string[] args = ["--number=not-a-number"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("9.4. 缺失必需参数时,抛出异常")] - public void MissingRequiredOption_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 10. 异步处理测试 - - [TestMethod("10.1. 异步处理方法,正确执行")] - public async Task AsyncHandler_ExecutesCorrectly() - { - // Arrange - string[] args = ["--value=async-test"]; - string? value = null; - - // Act - await CommandLine.Parse(args, Flexible) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - value = o.Value; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("async-test", value); - } - - #endregion - - #region 11. 列表参数测试 - - [TestMethod("11.1. 支持空列表")] - public void EmptyList_ParsedCorrectly() - { - // Arrange - string[] args = ["--files"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(0, files.Length); - } - - [TestMethod("11.3. 支持分号分隔的列表")] - public void SemicolonSeparatedList_ParsedCorrectly() - { - // Arrange - string[] args = ["--names:John;Jane;Doe"]; - IEnumerable? names = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - names = o.Names; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(names); - Assert.AreEqual(3, names.Count()); - CollectionAssert.AreEqual(new[] { "John", "Jane", "Doe" }, names.ToArray()); - } - - [TestMethod("11.4. 支持混合分隔符的列表")] - public void MixedSeparatorList_ParsedCorrectly() - { - // Arrange - string[] args = ["--files:file1.txt,file2.txt;file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("11.5. 支持带引号的列表参数")] - public void QuotedListElements_ParsedCorrectly() - { - // Arrange - string[] args = ["--files:\"file with spaces.txt\",\"another file.txt\""]; - string[]? files = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(2, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "another file.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("11.6. 带引号的列表参数,多次指定")] - public void QuotedListElements_MultipleOptions() - { - // Arrange - string[] args = ["--files", "\"file with spaces.txt\"", "--files", "normal.txt", "--files", "\"another file.txt\""]; - string[]? files = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("11.7. 带引号的列表参数,通过分隔符")] - public void QuotedListElements_WithSeparators() - { - // Arrange - string[] args = ["--names:\"John Doe\";\"Jane Smith\";Anonymous"]; - IEnumerable? names = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - names = o.Names; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(names); - Assert.AreEqual(3, names.Count()); - CollectionAssert.AreEqual(new[] { "John Doe", "Jane Smith", "Anonymous" }, names.ToArray()); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("11.9. 单选项后接带引号且引号内有逗号或分号的多个值")] - public void SingleOption_QuotedMultipleValuesWithColonOrSemicolon() - { - // Arrange - string[] args = ["--files", "\"tag1,with,colon\",\"tag2,with,colon\"", "--tags", "\"tag1;with;semicolon\";\"tag2;with;semicolon\""]; - string[]? files = null; - IReadOnlyList? tags = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - tags = o.Tags; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.IsNotNull(tags); - Assert.AreEqual(2, files.Length); - Assert.AreEqual(2, tags.Count); - CollectionAssert.AreEqual(new[] { "tag1,with,colon", "tag2,with,colon" }, files.ToArray()); - CollectionAssert.AreEqual(new[] { "tag1;with;semicolon", "tag2;with;semicolon" }, tags.ToArray()); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("11.10. 单选项后接带引号且引号内有逗号或分号的多个值,其中部分引号和分隔符含空字符串")] - public void SingleOption_QuotedMultipleValuesWithColonOrSemicolonAndEmpty() - { - // Arrange - string[] args = ["--files", "\"tag1,with,colon\",,\"tag2,with,colon\"", "--tags", "\"tag1;with;semicolon\";;;\"\";\"tag2;with;semicolon\""]; - string[]? files = null; - IReadOnlyList? tags = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - files = o.Files; - tags = o.Tags; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.IsNotNull(tags); - Assert.AreEqual(3, files.Length); - Assert.AreEqual(5, tags.Count); - CollectionAssert.AreEqual(new[] { "tag1,with,colon", "", "tag2,with,colon" }, files.ToArray()); - CollectionAssert.AreEqual(new[] { "tag1;with;semicolon", "", "", "", "tag2;with;semicolon" }, tags.ToArray()); - } - - #endregion -} - -#region 测试用数据模型 - -internal record Flexible01_StringOptions -{ - [Option] - public required string Value { get; init; } -} - -internal record Flexible02_ShortOption -{ - [Option('v')] - public required string V { get; init; } -} - -internal record Flexible03_MultipleShortOptions -{ - [Option('a')] - public required string A { get; init; } - - [Option('b')] - public required string B { get; init; } - - [Option('c')] - public required string C { get; init; } -} - -internal record Flexible04_KebabCaseOptions -{ - [Option("parameter-name")] - public required string ParameterName { get; init; } -} - -internal record Flexible05_ShortLongOptions -{ - [Option('o', "output")] - public required string Output { get; init; } -} - -internal record Flexible06_BooleanShortOptions -{ - [Option('a')] - public bool A { get; init; } - - [Option('b')] - public bool B { get; init; } - - [Option('c')] - public bool C { get; init; } -} - -internal record Flexible07_BooleanOptions -{ - [Option] - public bool Flag { get; init; } -} - -internal record Flexible08_NegatedBooleanOptions -{ - [Option([], ["feature", "no-feature"])] - public bool Feature { get; init; } = true; -} - -internal record Flexible09_PositionalOptions -{ - [Value] - public required string Value { get; init; } -} - -internal record Flexible10_MultiplePositionalOptions -{ - [Value(Length = int.MaxValue)] - public required string[] Values { get; init; } -} - -internal record Flexible11_TerminatorOptions -{ - [Option] - public required string Option { get; init; } - - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record Flexible12_MixedOptions -{ - [Value(0)] - public required string Value1 { get; init; } - - [Option] - public required string Option { get; init; } - - [Value(1)] - public required string Value2 { get; init; } -} - -internal record Flexible13_MixedPrefixOptions -{ - [Option] - public required string Option1 { get; init; } - - [Option] - public required string Option2 { get; init; } - - [Option] - public required string Option3 { get; init; } -} - -internal record Flexible14_MixedNamingOptions -{ - [Option("kebab-case")] - public required string KebabCase { get; init; } - - [Option("PascalCase")] - public required string PascalCase { get; init; } - - [Option("camelCase")] - public required string CamelCase { get; init; } -} - -internal record Flexible15_TypedOptions -{ - [Option] - public int Number { get; init; } -} - -internal record Flexible16_ListOptions -{ - [Option] - public string[] Files { get; init; } = []; - - [Option] - public IReadOnlyList Tags { get; init; } = []; - - [Option] - public IEnumerable Names { get; init; } = []; -} - -internal record Flexible16_RequiredOptions -{ - [Option] - public required string RequiredValue { get; init; } -} - -#endregion - -#region 代码清理 - -// ReSharper restore UnusedAutoPropertyAccessor.Global -// ReSharper restore InconsistentNaming - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs deleted file mode 100644 index 9b861f8c..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/GnuCommandLineParserTests.cs +++ /dev/null @@ -1,1120 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试GNU风格命令行参数是否正确被解析到了。 -/// -[TestClass] -public class GnuCommandLineParserTests -{ - private CommandLineParsingOptions GNU { get; } = CommandLineParsingOptions.Gnu; - - #region 1. 选项识别与解析 - - [TestMethod("1.1. 长选项,字符串类型,可正常赋值。")] - public void LongOption_StringType_ValueAssigned() - { - // Arrange - string[] args = ["--value", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.2. 短选项,字符串类型,可正常赋值。")] - public void ShortOption_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-v", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.3. 长选项带等号,字符串类型,可正常赋值。")] - public void LongOptionWithEquals_StringType_ValueAssigned() - { - // Arrange - string[] args = ["--value=test"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.4.1 短选项无空格,字符串类型,可正常赋值。")] - public void ShortOptionNoSpace_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-vtest.txt"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test.txt", value); - } - - [Ignore("目前先 Parse 后 As 的两个步骤,会使得第 1 步的 Parse 无法区分这种短选项无空格的值。1.4.1 因为带了非字母的符号所以还能勉强区分。除非我们未来在 CommandLine 对象里对同一个短选项存两种值才可能。")] - [TestMethod("1.4.2 短选项无空格,但难以与缩写区分,字符串类型,可正常赋值。")] - public void ShortOptionNoSpace2_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-vtest"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.5. 多个选项混合使用,全部正确解析。")] - public void MixedOptions_MultipleParsed_AllAssigned() - { - // Arrange - string[] args = - [ - "-n", "42", "-u", "11", "--text", "hello", "--nullable-text", "hello null", "--nullable-list", "a", "--nullable-nullable-list", "b", "-b" - ]; - int? number = null; - int? nullableNumber = null; - string? text = null; - string? nullableText = null; - IReadOnlyList? nullableList = null; - IReadOnlyList? nullableNullableList = null; - bool? flag = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - nullableNumber = o.NullableNumber; - number = o.Number; - text = o.Text; - nullableText = o.NullableText; - nullableList = o.NullableList; - nullableNullableList = o.NullableNullableList; - flag = o.Flag; - }) - .Run(); - - // Assert - Assert.AreEqual(11, nullableNumber); - Assert.AreEqual(42, number); - Assert.AreEqual("hello", text); - Assert.AreEqual("hello null", nullableText); - CollectionAssert.AreEqual(new[] { "a" }, nullableList?.ToList()); - CollectionAssert.AreEqual(new[] { "b" }, nullableNullableList?.ToList()); - Assert.IsTrue(flag); - } - - #endregion - - #region 2. 类型转换 - - [TestMethod("2.1. 整数类型,赋值成功。")] - public void IntegerOption_ValueAssigned() - { - // Arrange - string[] args = ["--number", "42"]; - int? number = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => number = o.Number) - .Run(); - - // Assert - Assert.AreEqual(42, number); - } - - [TestMethod("2.2. 布尔类型,赋值成功。")] - public void BooleanOption_ValueAssigned() - { - // Arrange - string[] args = ["--flag"]; - bool? flag = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => flag = o.Flag) - .Run(); - - // Assert - Assert.IsTrue(flag); - } - - [TestMethod("2.3. 枚举类型,赋值成功。")] - public void EnumOption_ValueAssigned() - { - // Arrange - string[] args = ["--log-level", "Warning", "--nullable-log-level", "Error"]; - LogLevel? logLevel = null; - LogLevel? nullableLogLevel = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - logLevel = o.LogLevel; - nullableLogLevel = o.NullableLogLevel; - }) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); - Assert.AreEqual(LogLevel.Error, nullableLogLevel); - } - - [TestMethod("2.4. 字符串数组,赋值成功。")] - public void StringArrayOption_ValueAssigned() - { - // Arrange - string[] args = ["--files", "file1.txt", "--files", "file2.txt", "--files", "file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [TestMethod("2.5. 列表类型,赋值成功。")] - public void ListOption_ValueAssigned() - { - // Arrange - string[] args = ["--tags", "tag1", "--tags", "tag2", "--tags", "tag3"]; - List? tags = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => tags = o.Tags.ToList()) - .Run(); - - // Assert - Assert.IsNotNull(tags); - Assert.AreEqual(3, tags.Count); - CollectionAssert.AreEqual(new[] { "tag1", "tag2", "tag3" }, tags); - } - - [TestMethod("2.6. 使用等号分隔的列表选项,通过分号划分")] - public void SemicolonSeparatedList_ValueAssigned() - { - // Arrange - string[] args = ["--files=file1.txt;file2.txt;file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [TestMethod("2.7. 使用等号分隔的列表选项,通过逗号划分")] - public void CommaSeparatedList_ValueAssigned() - { - // Arrange - string[] args = ["--files=file1.txt,file2.txt,file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.8. 带引号的列表参数,赋值成功。")] - public void QuotedArrayOption_ValueAssigned() - { - // Arrange - string[] args = ["--files", "\"file with spaces.txt\"", "--files", "normal.txt", "--files", "\"another file.txt\""]; - string[]? files = null; // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.9. 等号方式带引号的列表参数,赋值成功。")] - public void QuotedArrayWithEquals_ValueAssigned() - { - // Arrange - string[] args = ["--paths=\"path with spaces\",regular-path,\"another path\""]; - string[]? paths = null; // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - paths = o.Paths; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(paths); - Assert.AreEqual(3, paths.Length); - CollectionAssert.AreEqual(new[] { "path with spaces", "regular-path", "another path" }, paths); - } - - #endregion - - #region 3. 边界情况处理 - - [TestMethod("3.1. 缺失必需选项,抛出异常。")] - public void MissingRequiredOption_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, GNU) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("3.2. 无效格式选项,抛出异常。")] - public void InvalidOption_ThrowsException() - { - // Arrange - string[] args = ["---invalid"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, GNU) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("3.3. 类型不匹配,抛出异常。")] - public void TypeMismatch_ThrowsException() - { - // Arrange - string[] args = ["--number", "not-a-number"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, GNU) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("3.4. 大小写敏感,识别正确。")] - public void CaseSensitive_CorrectOptionParsed() - { - // Arrange - string[] args = ["--case-sensitive", "lower", "--CASE-SENSITIVE", "upper"]; - string? lowerValue = null; - string? upperValue = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) - .AddHandler(o => - { - lowerValue = o.CaseSensitive; - upperValue = o.CASESENSITIVE; - }) - .Run(); - - // Assert - Assert.AreEqual("lower", lowerValue); - Assert.AreEqual("upper", upperValue); - } - - [TestMethod("3.5. 大小写不敏感,识别正确。")] - public void CaseInsensitive_CorrectOptionParsed() - { - // Arrange - string[] args = ["--Ignore-Case", "value"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = false } }) - .AddHandler(o => value = o.IgnoreCase) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("3.6. 单个选项设置大小写敏感,全局默认不敏感,识别正确。")] - public void SingleOptionCaseSensitive_GlobalInsensitive_CorrectlyParsed() - { - // Arrange - string[] args = ["--Case-Option", "value1", "--case-option", "value2"]; - string? sensitiveValue = null; - string? insensitiveValue = null; - - // Act - CommandLine.Parse(args, GNU) // 默认大小写不敏感 - .AddHandler(o => - { - sensitiveValue = o.CaseSensitiveOption; - insensitiveValue = o.CaseInsensitiveOption; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", sensitiveValue); // 大小写敏感,匹配第一个 Case-Option - Assert.AreEqual("value2", insensitiveValue); // 大小写不敏感,匹配第二个 case-option - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.7. 单个选项设置大小写不敏感,全局设置为敏感,识别正确。")] - public void OptionCaseInsensitive_OverridesGlobalSensitive() - { - // Arrange - string[] args = ["--option-one", "value1", "--option-TWO", "value2"]; - string? option1Value = null; - string? option2Value = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) - .AddHandler(o => - { - option1Value = o.OptionOne; - option2Value = o.OptionTwo; - }) - .Run(); - - // Assert - Assert.IsNull(option1Value); // 全局大小写敏感,--option-one 不匹配 --Option-One - Assert.AreEqual("value2", option2Value); // 选项明确指定为大小写不敏感,所以匹配成功 - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.8. 全局大小写敏感时,未指定大小写设置的选项不匹配。")] - public void GlobalCaseSensitive_DefaultOption_NotMatched() - { - // Arrange - string[] args = ["--global-sensitive", "value1"]; - string? globalSensitiveValue = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) - .AddHandler(o => - { - globalSensitiveValue = o.GlobalSensitive; - }) - .Run(); - - // Assert - Assert.IsNull(globalSensitiveValue); // 全局大小写敏感,--global-sensitive 不匹配 --GLOBAL-SENSITIVE - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.9. 选项设置大小写敏感时,大小写不匹配无效。")] - public void OptionCaseSensitive_CaseMismatch_NotMatched() - { - // Arrange - string[] args = ["--local-sensitive", "value"]; - string? localSensitiveValue = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) - .AddHandler(o => - { - localSensitiveValue = o.LocalSensitive; - }) - .Run(); - - // Assert - Assert.IsNull(localSensitiveValue); // 局部大小写敏感,--local-sensitive 不匹配 --local-SENSITIVE - } - - [TestMethod("3.10. 选项设置大小写不敏感时,无论全局设置,都能匹配。")] - public void OptionCaseInsensitive_GlobalSensitive_StillMatched() - { - // Arrange - string[] args = ["--LOCAL-insensitive", "value"]; - string? localInsensitiveValue = null; - - // Act - CommandLine.Parse(args, GNU with { Style = GNU.Style with { CaseSensitive = true } }) - .AddHandler(o => - { - localInsensitiveValue = o.LocalInsensitive; - }) - .Run(); - - // Assert - Assert.AreEqual("value", localInsensitiveValue); // 明确指定大小写不敏感,匹配成功 - } - - [TestMethod("3.11. 选项值大小写测试,枚举值不敏感,识别正确。")] - public void EnumValueCaseInsensitive_CorrectlyParsed() - { - // Arrange - string[] args = ["--log-level", "warning", "--second-level", "ERROR"]; - LogLevel? logLevel = null; - LogLevel? secondLevel = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - logLevel = o.LogLevel; - secondLevel = o.SecondLevel; - }) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); // 枚举值大小写不敏感 - Assert.AreEqual(LogLevel.Error, secondLevel); // 枚举值大小写不敏感 - } - - #endregion - - #region 4. 特殊特性 - - [TestMethod("4.1. 选项别名,识别正确。")] - public void OptionAliases_CorrectOptionParsed() - { - // Arrange - string[] args = ["--alt", "value"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.OptionWithAlias) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("4.2. 组合短选项,识别正确。")] - public void CombinedShortOptions_AllParsedCorrectly() - { - // Arrange - string[] args = ["-abc"]; - bool? optionA = null; - bool? optionB = null; - bool? optionC = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - optionA = o.OptionA; - optionB = o.OptionB; - optionC = o.OptionC; - }) - .Run(); - - // Assert - Assert.IsTrue(optionA); - Assert.IsTrue(optionB); - Assert.IsTrue(optionC); - } - - [TestMethod("4.3. 终止选项解析符号,识别正确。")] - public void OptionTerminator_FollowingArgsAreValues() - { - // Arrange - string[] args = ["--option", "value", "--", "--not-an-option", "-x"]; - string? option = null; - string[]? values = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - option = o.Option; - values = o.Values; - }) - .Run(); - - // Assert - Assert.AreEqual("value", option); - Assert.IsNotNull(values); - Assert.AreEqual(2, values.Length); - Assert.AreEqual("--not-an-option", values[0]); - Assert.AreEqual("-x", values[1]); - } - - #endregion - - #region 5. 位置参数处理 - - [TestMethod("5.1. 单个位置参数,赋值成功。")] - public void SinglePositionalValue_ValueAssigned() - { - // Arrange - string[] args = ["positional-value"]; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("positional-value", value); - } - - [TestMethod("5.2. 多个位置参数,赋值成功。")] - public void MultiplePositionalValues_AllAssigned() - { - // Arrange - string[] args = ["value1", "value2", "value3"]; - string[]? values = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => values = o.Values) - .Run(); - - // Assert - Assert.IsNotNull(values); - Assert.AreEqual(3, values.Length); - CollectionAssert.AreEqual(new[] { "value1", "value2", "value3" }, values); - } - - [TestMethod("5.3. 位置参数与选项混合,识别正确。")] - public void MixedPositionalAndOptions_AllParsedCorrectly() - { - // Arrange - string[] args = ["value1", "--option", "opt-val", "value2"]; - string? option = null; - string? value1 = null; - string? value2 = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - option = o.Option; - value1 = o.Value1; - value2 = o.Value2; - }) - .Run(); - - // Assert - Assert.AreEqual("opt-val", option); - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("5.4. 指定索引位置参数,识别正确。")] - public void IndexedPositionalValues_CorrectAssignment() - { - // Arrange - string[] args = ["first", "second", "third"]; - string? first = null; - string? third = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - first = o.First; - third = o.Third; - }) - .Run(); - - // Assert - Assert.AreEqual("first", first); - Assert.AreEqual("third", third); - } - - #endregion - - #region 6. Required 和 Nullable 组合测试 - - [TestMethod("6.1. Non-required, Non-nullable, 无CLI参数,使用默认值。")] - public void NonRequiredNonNullable_NoCli_UsesDefault() - { - // Arrange - string[] args = []; - string? value = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual(null, value); // 使用初始化时的默认值 - } - - [TestMethod("6.2. Required, Non-nullable, 无CLI参数,抛出异常。")] - public void RequiredNonNullable_NoCli_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, GNU) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.3. Non-required, Nullable, 无CLI参数,赋默认值(null)。")] - public void NonRequiredNullable_NoCli_DefaultNull() - { - // Arrange - string[] args = []; - string? value = "not-null"; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.IsNull(value); - } - - [TestMethod("6.4. Required, Nullable, 无CLI参数,抛出异常。")] - public void RequiredNullable_NoCli_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, GNU) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.5. 各种组合都提供CLI参数,全部赋值成功。")] - public void AllCombinations_WithCli_AllAssigned() - { - // Arrange - string[] args = - [ - "--req-non-null", "value1", "--non-req-null", "value2", - "--req-null", "value3", "--non-req-non-null", "value4" - ]; - string? reqNonNull = null; - string? nonReqNull = null; - string? reqNull = null; - string? nonReqNonNull = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - reqNonNull = o.ReqNonNull; - nonReqNull = o.NonReqNull; - reqNull = o.ReqNull; - nonReqNonNull = o.NonReqNonNull; - }) - .Run(); - - // Assert - Assert.AreEqual("value1", reqNonNull); - Assert.AreEqual("value2", nonReqNull); - Assert.AreEqual("value3", reqNull); - Assert.AreEqual("value4", nonReqNonNull); - } - - [TestMethod("6.6. 可空枚举类型,无CLI参数,赋默认值(null)。")] - public void NullableEnumOption_NoCli_DefaultNull() - { - // Arrange - string[] args = ["--log-level", "Warning"]; // 只提供非可空枚举,不提供可空枚举 - LogLevel? logLevel = null; - LogLevel? nullableLogLevel = LogLevel.Error; // 初始化为非null值,验证会被设置为null - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - logLevel = o.LogLevel; - nullableLogLevel = o.NullableLogLevel; - }) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); - Assert.IsNull(nullableLogLevel, $"可空枚举应该是null,但实际值是: {nullableLogLevel}"); // 可空枚举应该是null - } - - [TestMethod("6.7. 可空值类型,无CLI参数,赋默认值(null)。")] - public void NullableValueTypes_NoCli_DefaultNull() - { - // Arrange - string[] args = ["--provided-int", "42"]; // 只提供一个必需的参数,不提供其他可空参数 - int? nullableInt = 123; // 初始化为非null值,验证会被设置为null - bool? nullableBool = true; // 初始化为非null值,验证会被设置为null - double? nullableDouble = 3.14; // 初始化为非null值,验证会被设置为null - decimal? nullableDecimal = 100.5m; // 初始化为非null值,验证会被设置为null - int? providedInt = null; - - // Act - CommandLine.Parse(args, GNU) - .AddHandler(o => - { - nullableInt = o.NullableInt; - nullableBool = o.NullableBool; - nullableDouble = o.NullableDouble; - nullableDecimal = o.NullableDecimal; - providedInt = o.ProvidedInt; - }) - .Run(); - - // Assert - Assert.AreEqual(42, providedInt); // 提供的参数应该正确解析 - Assert.IsNull(nullableInt, $"可空int应该是null,但实际值是: {nullableInt}"); - Assert.IsNull(nullableBool, $"可空bool应该是null,但实际值是: {nullableBool}"); - Assert.IsNull(nullableDouble, $"可空double应该是null,但实际值是: {nullableDouble}"); - Assert.IsNull(nullableDecimal, $"可空decimal应该是null,但实际值是: {nullableDecimal}"); - } - - #endregion - - #region 7. 异步处理测试 - - [TestMethod("7.1. 异步处理方法,正确执行。")] - public async Task AsyncHandler_ExecutesCorrectly() - { - // Arrange - string[] args = ["--value", "async-test"]; - string? value = null; - - // Act - await CommandLine.Parse(args, GNU) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - value = o.Value; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("async-test", value); - } - - #endregion -} - -#region 测试用数据模型 - -internal record GNU01_StringOptions -{ - [Option] - public required string Value { get; init; } -} - -internal record GNU02_ShortOptions -{ - [Option('v')] - public required string Value { get; init; } -} - -internal record GNU03_MixedOptions -{ - [Option('n')] - public int Number { get; init; } - - [Option('u')] - public int? NullableNumber { get; init; } - - [Option] - public required string Text { get; init; } - - [Option] - public required string? NullableText { get; init; } - - [Option] - public required IReadOnlyList NullableList { get; init; } - - [Option] - public required IReadOnlyList? NullableNullableList { get; init; } - - [Option('b')] - public bool Flag { get; init; } -} - -internal record GNU04_IntegerOptions -{ - [Option] - public int Number { get; init; } -} - -internal record GNU05_BooleanOptions -{ - [Option] - public bool Flag { get; init; } -} - -internal record GNU06_EnumOptions -{ - [Option("log-level")] - public LogLevel LogLevel { get; init; } - - [Option("nullable-log-level")] - public LogLevel? NullableLogLevel { get; init; } -} - -internal record GNU07_ArrayOptions -{ - [Option] - public string[] Files { get; init; } = []; -} - -internal record GNU08_ListOptions -{ - [Option] - public IReadOnlyList Tags { get; init; } = []; -} - -internal record GNU09_RequiredOptions -{ - [Option] - public required string RequiredValue { get; init; } -} - -internal record GNU10_CaseSensitiveOptions -{ - [Option("case-sensitive", CaseSensitive = true)] - public string CaseSensitive { get; init; } = string.Empty; - - [Option("CASE-SENSITIVE", CaseSensitive = true)] - public string CASESENSITIVE { get; init; } = string.Empty; -} - -internal record GNU11_CaseInsensitiveOptions -{ - [Option("ignore-case")] - public string IgnoreCase { get; init; } = string.Empty; -} - -internal record GNU12_AliasOptions -{ - [Option([], ["option-with-alias", "alt", "alternate"])] - public string OptionWithAlias { get; init; } = string.Empty; -} - -internal record GNU13_CombinedOptions -{ - [Option('a')] - public bool OptionA { get; init; } - - [Option('b')] - public bool OptionB { get; init; } - - [Option('c')] - public bool OptionC { get; init; } -} - -internal record GNU14_TerminatorOptions -{ - [Option] - public string Option { get; init; } = string.Empty; - - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record GNU15_SingleValueOptions -{ - [Value] - public string Value { get; init; } = string.Empty; -} - -internal record GNU16_MultipleValueOptions -{ - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record GNU17_MixedValueOptions -{ - [Value(0)] - public string Value1 { get; init; } = string.Empty; - - [Option] - public string Option { get; init; } = string.Empty; - - [Value(1)] - public string Value2 { get; init; } = string.Empty; -} - -internal record GNU18_IndexedValueOptions -{ - [Value(0)] - public string First { get; init; } = string.Empty; - - [Value(2)] - public string Third { get; init; } = string.Empty; -} - -internal record GNU19_RequiredNonNullableOption -{ - [Option] - public required string Value { get; init; } -} - -internal record GNU20_NonRequiredNullableOption -{ - [Option] - public string? Value { get; init; } -} - -internal record GNU21_RequiredNullableOption -{ - [Option] - public required string? Value { get; init; } -} - -internal record GNU22_AllCombinationsOption -{ - [Option("req-non-null")] - public required string ReqNonNull { get; init; } - - [Option("non-req-null")] - public string? NonReqNull { get; init; } - - [Option("req-null")] - public required string? ReqNull { get; init; } - - [Option("non-req-non-null")] - public string NonReqNonNull { get; init; } = string.Empty; -} - -internal record GNU23_MixedCaseOptions -{ - [Option("Case-Option", CaseSensitive = true)] - public string CaseSensitiveOption { get; init; } = string.Empty; - - [Option("case-option")] - public string CaseInsensitiveOption { get; init; } = string.Empty; -} - -internal record GNU24_OverrideCaseOptions -{ - [Option("Option-One")] - public string OptionOne { get; init; } = string.Empty; - - [Option("option-TWO", CaseSensitive = false)] - public string OptionTwo { get; init; } = string.Empty; -} - -internal record GNU25_ComplexCaseOptions -{ - [Option("GLOBAL-SENSITIVE")] - public string GlobalSensitive { get; init; } = string.Empty; - - [Option("local-SENSITIVE", CaseSensitive = true)] - public string LocalSensitive { get; init; } = string.Empty; - - [Option("Local-Insensitive", CaseSensitive = false)] - public string LocalInsensitive { get; init; } = string.Empty; -} - -internal record GNU26_EnumCaseOptions -{ - [Option("log-level")] - public LogLevel LogLevel { get; init; } - - [Option("second-level")] - public LogLevel SecondLevel { get; init; } -} - -internal record GNU27_NonRequiredNonNullableOption -{ - [Option] - public string Value { get; init; } = string.Empty; -} - -internal record GNU28_NullableValueTypesOptions -{ - [Option("nullable-int")] - public int? NullableInt { get; init; } - - [Option("nullable-bool")] - public bool? NullableBool { get; init; } - - [Option("nullable-double")] - public double? NullableDouble { get; init; } - - [Option("nullable-decimal")] - public decimal? NullableDecimal { get; init; } - - [Option("provided-int")] - public int ProvidedInt { get; init; } -} - -internal record GNU14_QuotedArrayOptions -{ - [Option] - public string[] Files { get; init; } = []; - - [Option] - public string[] Paths { get; init; } = []; -} - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/LogLevel.cs b/tests/DotNetCampus.CommandLine.Tests/LogLevel.cs deleted file mode 100644 index d6569fb7..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/LogLevel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DotNetCampus.Cli.Tests; - -internal enum LogLevel -{ - Debug, - Info, - Warning, - Error, - Critical -} diff --git a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs b/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs deleted file mode 100644 index 2e986e48..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/NamingConventionTests.cs +++ /dev/null @@ -1,718 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试命名规则(Naming Convention)功能,包括 kebab-case、PascalCase、camelCase 等多种命名风格的支持。 -/// 基于 CommandAttribute、OptionAttribute 和 ValueAttribute 的命名规则要求。 -/// -[TestClass] -public class NamingConventionTests -{ - private CommandLineParsingOptions Flexible { get; } = CommandLineParsingOptions.Flexible; - - private CommandLineParsingOptions CaseSensitive { get; } = new CommandLineParsingOptions - { - Style = CommandLineParsingOptions.Flexible.Style with { CaseSensitive = true }, - }; - - #region 1. CommandAttribute 命名规则测试 - - [TestMethod("1.1. kebab-case 命令名称 - 基本情况")] - public void Command_KebabCase_BasicCase() - { - // Arrange - string[] args = ["build-project", "--verbose"]; - bool handlerCalled = false; - bool verboseFlag = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - handlerCalled = true; - verboseFlag = o.Verbose; - }) - .Run(); - - // Assert - Assert.IsTrue(handlerCalled); - Assert.IsTrue(verboseFlag); - } - - [TestMethod("1.2. kebab-case 多级子命令")] - public void Command_KebabCase_MultiLevel() - { - // Arrange - string[] args = ["user-management", "create-account", "--username", "john"]; - string? capturedUsername = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedUsername = o.Username; - }) - .Run(); - - // Assert - Assert.AreEqual("john", capturedUsername); - } - - [TestMethod("1.3. 空命令名称 - 默认命令")] - public void Command_EmptyName_DefaultCommand() - { - // Arrange - string[] args = ["--help"]; - bool handlerCalled = false; - bool helpFlag = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - handlerCalled = true; - helpFlag = o.Help; - }) - .Run(); - - // Assert - Assert.IsTrue(handlerCalled); - Assert.IsTrue(helpFlag); - } - - [TestMethod("1.4. 单一命令名称")] - public void Command_SingleName() - { - // Arrange - string[] args = ["build", "--output", "bin"]; - string? capturedOutput = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedOutput = o.Output; - }) - .Run(); - - // Assert - Assert.AreEqual("bin", capturedOutput); - } - - [TestMethod("1.5. 命令名称大小写不敏感(默认)")] - public void Command_CaseInsensitive_Default() - { - // Arrange - string[] args = ["BUILD-PROJECT", "--verbose"]; // 大写命令 - bool handlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) // Flexible 默认大小写不敏感 - .AddHandler(_ => handlerCalled = true) - .Run(); - - // Assert - Assert.IsTrue(handlerCalled); - } - - [TestMethod("1.6. 命令名称大小写敏感")] - public void Command_CaseSensitive() - { - // Arrange - string[] args = ["BUILD-PROJECT", "--verbose"]; // 大写命令 - - // Act & Assert - 大小写敏感模式下应该抛出异常 - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, CaseSensitive) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 2. OptionAttribute 命名规则测试 - - [TestMethod("2.1. 无参数 OptionAttribute - 自动使用属性名")] - public void Option_NoParameter_UsePropertyName() - { - // Arrange - string[] args = ["--verbose"]; // 属性名 Verbose 转为 kebab-case - bool verboseFlag = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - verboseFlag = o.Verbose; - }) - .Run(); - - // Assert - Assert.IsTrue(verboseFlag); - } - - [TestMethod("2.2. kebab-case 长选项名")] - public void Option_KebabCase_LongName() - { - // Arrange - string[] args = ["--output-directory", "bin"]; - string? capturedOutput = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedOutput = o.OutputDirectory; - }) - .Run(); - - // Assert - Assert.AreEqual("bin", capturedOutput); - } - - [TestMethod("2.3. 短选项名(单字符)")] - public void Option_ShortName_SingleCharacter() - { - // Arrange - string[] args = ["-v"]; // 短选项 - bool verboseFlag = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - verboseFlag = o.Verbose; - }) - .Run(); - - // Assert - Assert.IsTrue(verboseFlag); - } - - [TestMethod("2.4. 短选项名和长选项名组合")] - public void Option_ShortAndLongName_Combined() - { - // Arrange - 测试短选项 - string[] shortArgs = ["-o", "bin"]; - string? capturedOutputShort = null; - - // Act - CommandLine.Parse(shortArgs, Flexible) - .AddHandler(o => - { - capturedOutputShort = o.Output; - }) - .Run(); - - // Assert - Assert.AreEqual("bin", capturedOutputShort); - - // Arrange - 测试长选项 - string[] longArgs = ["--output", "lib"]; - string? capturedOutputLong = null; - - // Act - CommandLine.Parse(longArgs, Flexible) - .AddHandler(o => - { - capturedOutputLong = o.Output; - }) - .Run(); - - // Assert - Assert.AreEqual("lib", capturedOutputLong); - } - - [TestMethod("2.5. 选项别名(Aliases)")] - public void Option_Aliases() - { - // Arrange - 测试第一个别名 - string[] aliasArgs1 = ["--out", "bin"]; - string? capturedOutput1 = null; - - // Act - CommandLine.Parse(aliasArgs1, Flexible) - .AddHandler(o => - { - capturedOutput1 = o.OutputPath; - }) - .Run(); - - // Assert - Assert.AreEqual("bin", capturedOutput1); - - // Arrange - 测试第二个别名 - string[] aliasArgs2 = ["--directory", "lib"]; - string? capturedOutput2 = null; - - // Act - CommandLine.Parse(aliasArgs2, Flexible) - .AddHandler(o => - { - capturedOutput2 = o.OutputPath; - }) - .Run(); - - // Assert - Assert.AreEqual("lib", capturedOutput2); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.6. ExactSpelling 精确拼写")] - public void Option_ExactSpelling() - { - // Arrange - 使用精确拼写的选项名 - string[] exactArgs = ["--SampleProperty", "test"]; - string? capturedValue = null; - - // Act - CommandLine.Parse(exactArgs, CommandLineParsingOptions.Gnu) - .AddHandler(o => - { - capturedValue = o.SampleProperty; - }) - .Run(); - - // Assert - Assert.AreEqual("test", capturedValue); - - // Arrange - 尝试使用自动转换的名称(应该失败) - string[] kebabArgs = ["--sample-property", "test"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(kebabArgs, CommandLineParsingOptions.Gnu) - .AddHandler(_ => { }) - .Run(); - }); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("2.7. 选项大小写敏感性")] - public void Option_CaseSensitive() - { - // Arrange - string[] args = ["--VERBOSE"]; // 大写选项 - - // Act - 大小写不敏感模式(默认) - bool verboseFlexible = false; - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - verboseFlexible = o.Verbose; - }) - .Run(); - - // Assert - Assert.IsTrue(verboseFlexible); - - // Act & Assert - 大小写敏感模式 - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, CaseSensitive) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 3. ValueAttribute 命名规则测试 - - [TestMethod("3.1. 无参数 ValueAttribute - 默认索引 0")] - public void Value_NoParameter_DefaultIndex() - { - // Arrange - string[] args = ["input.txt"]; - string? capturedInput = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedInput = o.InputFile; - }) - .Run(); - - // Assert - Assert.AreEqual("input.txt", capturedInput); - } - - [TestMethod("3.2. 指定索引的 ValueAttribute")] - public void Value_SpecificIndex() - { - // Arrange - string[] args = ["source.txt", "destination.txt"]; - string? capturedSource = null; - string? capturedDestination = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedSource = o.Source; - capturedDestination = o.Destination; - }) - .Run(); - - // Assert - Assert.AreEqual("source.txt", capturedSource); - Assert.AreEqual("destination.txt", capturedDestination); - } - - [TestMethod("3.3. 可变长度 ValueAttribute")] - public void Value_VariableLength() - { - // Arrange - string[] args = ["file1.txt", "file2.txt", "file3.txt"]; - string[]? capturedFiles = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedFiles = o.Files; - }) - .Run(); - - // Assert - Assert.IsNotNull(capturedFiles); - Assert.AreEqual(3, capturedFiles.Length); - Assert.AreEqual("file1.txt", capturedFiles[0]); - Assert.AreEqual("file2.txt", capturedFiles[1]); - Assert.AreEqual("file3.txt", capturedFiles[2]); - } - - [TestMethod("3.4. 指定索引和长度的 ValueAttribute")] - public void Value_SpecificIndexAndLength() - { - // Arrange - string[] args = ["cmd", "arg1", "arg2", "remaining"]; - string? capturedCommand = null; - string[]? capturedArgs = null; - string? capturedRemaining = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedCommand = o.Command; - capturedArgs = o.Arguments; - capturedRemaining = o.Remaining; - }) - .Run(); - - // Assert - Assert.AreEqual("cmd", capturedCommand); - Assert.IsNotNull(capturedArgs); - Assert.AreEqual(2, capturedArgs.Length); - Assert.AreEqual("arg1", capturedArgs[0]); - Assert.AreEqual("arg2", capturedArgs[1]); - Assert.AreEqual("remaining", capturedRemaining); - } - - #endregion - - #region 4. 混合命名风格测试 - - [TestMethod("4.1. PascalCase 属性名自动转换为 kebab-case")] - public void Mixed_PascalCaseToKebabCase() - { - // Arrange - string[] args = ["--sample-property", "test", "--another-option", "value"]; - string? capturedSample = null; - string? capturedAnother = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedSample = o.SampleProperty; - capturedAnother = o.AnotherOption; - }) - .Run(); - - // Assert - Assert.AreEqual("test", capturedSample); - Assert.AreEqual("value", capturedAnother); - } - - [TestMethod("4.2. camelCase 属性名自动转换为 kebab-case")] - public void Mixed_CamelCaseToKebabCase() - { - // Arrange - string[] args = ["--my-option", "test", "--another-value", "value"]; - string? capturedOption = null; - string? capturedValue = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedOption = o.myOption; - capturedValue = o.anotherValue; - }) - .Run(); - - // Assert - Assert.AreEqual("test", capturedOption); - Assert.AreEqual("value", capturedValue); - } - - [TestMethod("4.3. 多种命名风格的兼容性")] - public void Mixed_MultipleNamingStyles() - { - // Arrange - 测试不同的输入风格都能被识别 - string[] kebabArgs = ["--output-directory", "bin"]; - string[] pascalArgs = ["--OutputDirectory", "lib"]; - string? capturedKebab = null; - string? capturedPascal = null; - - // Act - kebab-case 输入 - CommandLine.Parse(kebabArgs, Flexible) - .AddHandler(o => - { - capturedKebab = o.OutputDirectory; - }) - .Run(); - - // Act - PascalCase 输入(在非精确拼写模式下应该也能工作) - CommandLine.Parse(pascalArgs, Flexible) - .AddHandler(o => - { - capturedPascal = o.OutputDirectory; - }) - .Run(); - - // Assert - Assert.AreEqual("bin", capturedKebab); - Assert.AreEqual("lib", capturedPascal); - } - - #endregion - - #region 5. 边界情况和错误处理测试 - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("5.1. 无效的短选项名(非字母字符)")] - public void Error_InvalidShortOptionName() - { - // 这个测试主要验证 OptionAttribute 构造函数的参数验证 - // 在实际使用中,这会在编译时就报错,所以我们这里测试运行时的行为 - - // Act & Assert - Assert.ThrowsExactly(() => - { - var _ = new OptionAttribute('1'); // 数字不是有效的短选项名 - }); - - Assert.ThrowsExactly(() => - { - var _ = new OptionAttribute('-'); // 符号不是有效的短选项名 - }); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("5.2. 空字符串选项名")] - public void Error_EmptyOptionName() - { - // Arrange - string[] args = ["test"]; - - // Act & Assert - 空的选项名应该被忽略或使用属性名 - // 这里我们测试使用空字符串作为选项名时的行为 - bool handlerCalled = false; - CommandLine.Parse(args, Flexible) - .AddHandler(_ => handlerCalled = true) - .Run(); - - Assert.IsTrue(handlerCalled); - } - - [TestMethod("5.3. 重复的 ValueAttribute 索引")] - public void Error_DuplicateValueIndex() - { - // Arrange - string[] args = ["value"]; - - // Act & Assert - 这种情况下应该根据实现决定如何处理 - // 通常最后一个定义会生效或者抛出异常 - bool handlerCalled = false; - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - handlerCalled = true; - // 验证至少有一个值被设置 - Assert.IsTrue(!string.IsNullOrEmpty(o.Value1) || !string.IsNullOrEmpty(o.Value2)); - }) - .Run(); - - Assert.IsTrue(handlerCalled); - } - - #endregion -} - -#region 测试用数据模型 - -// 1. CommandAttribute 测试类 - -[Command("build-project")] -internal class BuildProjectCommand -{ - [Option("verbose")] - public bool Verbose { get; init; } -} - -[Command("user-management create-account")] -internal class UserManagementCreateAccountCommand -{ - [Option("username")] - public required string Username { get; init; } -} - -[Command(null)] // 无参数,表示默认命令 -internal class DefaultCommand -{ - [Option("help")] - public bool Help { get; init; } - - [Option] // 无参数,使用属性名 - public bool Verbose { get; init; } -} - -[Command("build")] -internal class BuildCommand -{ - [Option("output")] - public required string Output { get; init; } -} - -// 2. OptionAttribute 测试类 - -internal class BuildWithOptionsCommand -{ - [Option("output-directory")] - public required string OutputDirectory { get; init; } -} - -internal class BuildWithShortOptionsCommand -{ - [Option('v')] - public bool Verbose { get; init; } -} - -internal class BuildWithCombinedOptionsCommand -{ - [Option('o', "output")] - public required string Output { get; init; } -} - -internal class BuildWithAliasesCommand -{ - [Option([], ["output-path", "out", "directory"])] - public required string OutputPath { get; init; } -} - -internal class ExactSpellingCommand -{ - [Option("SampleProperty")] - public required string SampleProperty { get; init; } -} - -internal class DefaultCaseSensitiveOptionsCommand -{ - [Option("verbose")] - public required bool Verbose { get; init; } -} - -// 3. ValueAttribute 测试类 - -internal class FileProcessCommand -{ - [Value] // 默认索引 0 - public required string InputFile { get; init; } -} - -internal class FileCopyCommand -{ - [Value(0)] - public required string Source { get; init; } - - [Value(1)] - public required string Destination { get; init; } -} - -internal class MultiFileCommand -{ - [Value(Length = int.MaxValue)] - public required string[] Files { get; init; } -} - -internal class ComplexValueCommand -{ - [Value(0)] - public required string Command { get; init; } - - [Value(1, 2)] - public required string[] Arguments { get; init; } - - [Value(3)] - public required string Remaining { get; init; } -} - -// 4. 混合命名风格测试类 - -internal class MixedNamingCommand -{ - [Option] // 使用属性名,PascalCase -> kebab-case - public required string SampleProperty { get; init; } - - [Option] // 使用属性名,PascalCase -> kebab-case - public required string AnotherOption { get; init; } -} - -internal class CamelCaseNamingCommand -{ - [Option] // camelCase -> kebab-case - public required string myOption { get; init; } - - [Option] // camelCase -> kebab-case - public required string anotherValue { get; init; } -} - -internal class CompatibilityCommand -{ - [Option] // 应该支持多种输入风格 - public required string OutputDirectory { get; init; } -} - -// 5. 边界情况测试类 - -internal class EmptyOptionNameCommand -{ - [Option("")] // 空字符串选项名 - public string EmptyName { get; init; } = ""; -} - -internal class DuplicateValueIndexCommand -{ - [Value(0)] - public string Value1 { get; init; } = ""; - - [Value(0)] // 重复的索引 - public string Value2 { get; init; } = ""; -} - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs deleted file mode 100644 index 73315530..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/PosixCommandLineParserTests.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试POSIX风格命令行参数是否正确被解析。 -/// -[TestClass] -public class PosixCommandLineParserTests -{ - private CommandLineParsingOptions POSIX { get; } = CommandLineParsingOptions.Posix; - - #region 1. 基本短选项解析 - - [TestMethod("1.1. 单个短选项,字符串类型,可正常赋值。")] - public void SingleShortOption_StringType_ValueAssigned() - { - // Arrange - string[] args = ["-v", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("1.2. 带参数的短选项,数值类型,可正常赋值。")] - public void ShortOptionWithValue_IntType_ValueAssigned() - { - // Arrange - string[] args = ["-n", "42"]; - int? number = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => number = o.Number) - .Run(); - - // Assert - Assert.AreEqual(42, number); - } - - [TestMethod("1.3. 多个短选项,全部正确解析。")] - public void MultipleShortOptions_AllParsed() - { - // Arrange - string[] args = ["-v", "text", "-n", "42", "-f"]; - string? value = null; - int? number = null; - bool? flag = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => - { - value = o.Value; - number = o.Number; - flag = o.Flag; - }) - .Run(); - - // Assert - Assert.AreEqual("text", value); - Assert.AreEqual(42, number); - Assert.IsTrue(flag); - } - - [TestMethod("1.4. 短选项无空格跟参数 (不支持) 。")] - public void ShortOptionNoSpace_NotSupported_ThrowsException() - { - // Arrange - string[] args = ["-vtest.txt"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 2. 组合短选项 - - [TestMethod("2.1. 组合布尔短选项,全部正确解析。")] - public void CombinedShortOptions_BooleanFlags_AllAssigned() - { - // Arrange - string[] args = ["-abc"]; - bool? optionA = null; - bool? optionB = null; - bool? optionC = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => - { - optionA = o.OptionA; - optionB = o.OptionB; - optionC = o.OptionC; - }) - .Run(); - - // Assert - Assert.IsTrue(optionA); - Assert.IsTrue(optionB); - Assert.IsTrue(optionC); - } - - [TestMethod("2.2. 组合短选项中,最后一个带参数会抛异常。")] - public void CombinedShortOptions_LastWithParam_ThrowsException() - { - // Arrange - string[] args = ["-abc", "value"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 3. 选项终止符(--) - - [TestMethod("3.1. 终止符后的参数被当作位置参数处理。")] - public void OptionTerminator_FollowingArgsAreValues() - { - // Arrange - string[] args = ["-o", "value", "--", "-x", "-y"]; - string? option = null; - string[]? values = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => - { - option = o.Option; - values = o.Values; - }) - .Run(); - - // Assert - Assert.AreEqual("value", option); - Assert.IsNotNull(values); - Assert.AreEqual(2, values.Length); - Assert.AreEqual("-x", values[0]); - Assert.AreEqual("-y", values[1]); - } - - #endregion - - #region 4. 位置参数处理 - - [TestMethod("4.1. 单个位置参数,赋值成功。")] - public void SinglePositionalValue_ValueAssigned() - { - // Arrange - string[] args = ["positional-value"]; - string? value = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("positional-value", value); - } - - [TestMethod("4.2. 多个位置参数,赋值成功。")] - public void MultiplePositionalValues_AllAssigned() - { - // Arrange - string[] args = ["value1", "value2", "value3"]; - string[]? values = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => values = o.Values) - .Run(); - - // Assert - Assert.IsNotNull(values); - Assert.AreEqual(3, values.Length); - CollectionAssert.AreEqual(new[] { "value1", "value2", "value3" }, values); - } - - [TestMethod("4.3. 位置参数与选项混合,识别正确。")] - public void MixedPositionalAndOptions_AllParsedCorrectly() - { - // Arrange - string[] args = ["value1", "-o", "opt-val", "value2"]; - string? option = null; - string? value1 = null; - string? value2 = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => - { - option = o.Option; - value1 = o.Value1; - value2 = o.Value2; - }) - .Run(); - - // Assert - Assert.AreEqual("opt-val", option); - Assert.AreEqual("value1", value1); - Assert.AreEqual("value2", value2); - } - - #endregion - - #region 5. 边界情况测试 - - [TestMethod("5.1. 缺失必需选项,抛出异常。")] - public void MissingRequiredOption_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("5.2. 无效选项格式,抛出异常。")] - public void InvalidOption_ThrowsException() - { - // Arrange - string[] args = ["-invalid-format"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("5.3. 类型不匹配,抛出异常。")] - public void TypeMismatch_ThrowsException() - { - // Arrange - string[] args = ["-n", "not-a-number"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("5.4. 不允许长选项,抛出异常。")] - public void LongOption_NotSupported_ThrowsException() - { - // Arrange - string[] args = ["--option", "value"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, POSIX) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 6. 异步处理测试 - - [TestMethod("6.1. 异步处理方法,正确执行。")] - public async Task AsyncHandler_ExecutesCorrectly() - { - // Arrange - string[] args = ["-v", "async-test"]; - string? value = null; - - // Act - await CommandLine.Parse(args, POSIX) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - value = o.Value; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("async-test", value); - } - - #endregion - - #region 7. 列表参数测试 - - [TestMethod("7.1. 多次指定同一选项形成列表")] - public void MultipleOptions_FormList() - { - // Arrange - string[] args = ["-f", "file1.txt", "-f", "file2.txt", "-f", "file3.txt"]; - string[]? files = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => files = o.Files) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file1.txt", "file2.txt", "file3.txt" }, files); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("7.2. 带引号的列表参数")] - public void QuotedArrayElements() - { - // Arrange - string[] args = ["-f", "\"file with spaces.txt\"", "-f", "normal.txt", "-f", "\"another file.txt\""]; - string[]? files = null; - - // Act - CommandLine.Parse(args, POSIX) - .AddHandler(o => - { - files = o.Files; - return 0; - }) - .Run(); - - // Assert - Assert.IsNotNull(files); - Assert.AreEqual(3, files.Length); - CollectionAssert.AreEqual(new[] { "file with spaces.txt", "normal.txt", "another file.txt" }, files); - } - - #endregion -} - -#region 测试用数据模型 - -internal record POSIX01_ShortOptions -{ - [Option('v')] - public required string Value { get; init; } -} - -internal record POSIX02_IntegerOptions -{ - [Option('n')] - public int Number { get; init; } -} - -internal record POSIX03_MixedOptions -{ - [Option('v')] - public required string Value { get; init; } - - [Option('n')] - public int Number { get; init; } - - [Option('f')] - public bool Flag { get; init; } -} - -internal record POSIX04_CombinedOptions -{ - [Option('a')] - public bool OptionA { get; init; } - - [Option('b')] - public bool OptionB { get; init; } - - [Option('c')] - public bool OptionC { get; init; } -} - -internal record POSIX05_CombinedWithValueOptions -{ - [Option('a')] - public bool OptionA { get; init; } - - [Option('b')] - public bool OptionB { get; init; } - - [Option('c')] - public required string OptionC { get; init; } -} - -internal record POSIX06_TerminatorOptions -{ - [Option('o')] - public string Option { get; init; } = string.Empty; - - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record POSIX07_SingleValueOptions -{ - [Value] - public string Value { get; init; } = string.Empty; -} - -internal record POSIX08_MultipleValueOptions -{ - [Value(Length = int.MaxValue)] - public string[] Values { get; init; } = []; -} - -internal record POSIX09_MixedValueOptions -{ - [Value(0)] - public string Value1 { get; init; } = string.Empty; - - [Option('o')] - public string Option { get; init; } = string.Empty; - - [Value(1)] - public string Value2 { get; init; } = string.Empty; -} - -internal record POSIX10_RequiredOptions -{ - [Option('r')] - public required string RequiredValue { get; init; } -} - -internal record POSIX11_LongOptionTest -{ - [Option("option")] // 这个会被POSIX风格拒绝,因为POSIX不支持长选项 - public string LongOption { get; init; } = string.Empty; -} - -internal record POSIX12_ArrayOptions -{ - [Option('f')] - public string[] Files { get; init; } = []; -} - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs b/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs deleted file mode 100644 index 3e8d3ed1..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/PowerShellCommandLineParserTests.cs +++ /dev/null @@ -1,663 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试PowerShell风格命令行参数是否正确被解析到了。 -/// -[TestClass] -public class PowerShellCommandLineParserTests -{ - private CommandLineParsingOptions PowerShell { get; } = CommandLineParsingOptions.PowerShell; - - #region 1. 基本参数解析 - - [TestMethod("1.1. 单个参数解析,字符串类型,Pascal命名。")] - public void SingleParameter_StringType_PascalNaming() - { - // Arrange - string[] args = ["-Name", "test"]; - string? name = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => name = o.Name) - .Run(); - - // Assert - Assert.AreEqual("test", name); - } - - [TestMethod("1.2. 多个参数解析,混合类型。")] - public void MultipleParameters_MixedTypes() - { - // Arrange - string[] args = ["-Path", "C:\\temp", "-ItemType", "Directory", "-Force"]; - string? path = null; - string? itemType = null; - bool? force = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - path = o.Path; - itemType = o.ItemType; - force = o.Force; - }) - .Run(); - - // Assert - Assert.AreEqual("C:\\temp", path); - Assert.AreEqual("Directory", itemType); - Assert.IsTrue(force); - } - - [TestMethod("1.3. 参数名使用Camel命名。")] - public void Parameter_CamelCase_ValueAssigned() - { - // Arrange - string[] args = ["-fileName", "document.txt"]; - string? fileName = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => fileName = o.fileName) - .Run(); - - // Assert - Assert.AreEqual("document.txt", fileName); - } - - #endregion - - #region 2. 开关参数处理 - - [TestMethod("2.1. 单个开关参数,布尔类型。")] - public void SwitchParameter_BooleanType_True() - { - // Arrange - string[] args = ["-Verbose"]; - bool? verbose = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => verbose = o.Verbose) - .Run(); - - // Assert - Assert.IsTrue(verbose); - } - - [TestMethod("2.2. 多个开关参数,全部为true。")] - public void MultipleSwitchParameters_AllTrue() - { - // Arrange - string[] args = ["-Recurse", "-Force", "-WhatIf"]; - bool? recurse = null; - bool? force = null; - bool? whatIf = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - recurse = o.Recurse; - force = o.Force; - whatIf = o.WhatIf; - }) - .Run(); - - // Assert - Assert.IsTrue(recurse); - Assert.IsTrue(force); - Assert.IsTrue(whatIf); - } - - [TestMethod("2.3. 开关参数与值参数混合。")] - public void MixedSwitchAndValueParameters() - { - // Arrange - string[] args = ["-Path", "logs.txt", "-Append", "-Encoding", "UTF8"]; - string? path = null; - bool? append = null; - string? encoding = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - path = o.Path; - append = o.Append; - encoding = o.Encoding; - }) - .Run(); - - // Assert - Assert.AreEqual("logs.txt", path); - Assert.IsTrue(append); - Assert.AreEqual("UTF8", encoding); - } - - #endregion - - #region 3. 参数名称缩写 - - [Ignore("暂时不打算支持 PowerShell 最短缩写功能,如果后面有需要再说。")] - [TestMethod("3.1. 使用参数的唯一缩写。")] - public void ParameterAbbreviation_UniquePrefix() - { - // Arrange - string[] args = ["-Com", "Server01"]; - string? computerName = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => computerName = o.ComputerName) - .Run(); - - // Assert - Assert.AreEqual("Server01", computerName); - } - - [TestMethod("3.2. 使用完整参数名。")] - public void ParameterAbbreviation_FullName() - { - // Arrange - string[] args = ["-ComputerName", "Server01"]; - string? computerName = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => computerName = o.ComputerName) - .Run(); - - // Assert - Assert.AreEqual("Server01", computerName); - } - - [Ignore("暂时不打算支持 PowerShell 最短缩写功能,如果后面有需要再说。")] - [TestMethod("3.3. 使用最短唯一缩写。")] - public void ParameterAbbreviation_ShortestUniquePrefix() - { - // Arrange - string[] args = ["-C", "Server01"]; - string? computerName = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => computerName = o.ComputerName) - .Run(); - - // Assert - Assert.AreEqual("Server01", computerName); - } - - [Ignore("暂时不打算支持 PowerShell 最短缩写功能,如果后面有需要再说。")] - [TestMethod("3.3. 缩写歧义处理。")] - public void ParameterAbbreviation_AmbiguousPrefix_ThrowsException() - { - // Arrange - string[] args = ["-Co", "value"]; // 可能是 ComputerName 或 Count - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, PowerShell) - .AddHandler(_ => { }) - .Run(); - }); - } - - #endregion - - #region 4. 位置参数 - - [TestMethod("4.1. 单个位置参数。")] - public void SinglePositionalParameter() - { - // Arrange - string[] args = ["value"]; - string? value = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => value = o.Value) - .Run(); - - // Assert - Assert.AreEqual("value", value); - } - - [TestMethod("4.2. 多个位置参数。")] - public void MultiplePositionalParameters() - { - // Arrange - string[] args = ["source.txt", "destination.txt"]; - string? source = null; - string? destination = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - source = o.Source; - destination = o.Destination; - }) - .Run(); - - // Assert - Assert.AreEqual("source.txt", source); - Assert.AreEqual("destination.txt", destination); - } - - [TestMethod("4.3. 位置参数与命名参数混合。")] - public void MixedPositionalAndNamedParameters() - { - // Arrange - string[] args = ["source.txt", "-Destination", "dest.txt", "-Force"]; - string? source = null; - string? destination = null; - bool? force = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - source = o.Source; - destination = o.Destination; - force = o.Force; - }) - .Run(); - - // Assert - Assert.AreEqual("source.txt", source); - Assert.AreEqual("dest.txt", destination); - Assert.IsTrue(force); - } - - #endregion - - #region 5. 数组参数 - - [TestMethod("5.1. 逗号分隔的数组参数。")] - public void CommaSeparatedArrayParameter() - { - // Arrange - string[] args = ["-Processes", "chrome,firefox,edge"]; - string[]? processes = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => processes = o.Processes) - .Run(); - - // Assert - Assert.IsNotNull(processes); - Assert.AreEqual(3, processes.Length); - CollectionAssert.AreEqual(new[] { "chrome", "firefox", "edge" }, processes); - } - - [TestMethod("5.2. 多次指定同一参数形成数组。")] - public void RepeatedParameterAsArray() - { - // Arrange - string[] args = ["-ComputerName", "server1", "-ComputerName", "server2", "-ComputerName", "server3"]; - IReadOnlyList? computerNames = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => computerNames = o.ComputerName) - .Run(); - - // Assert - Assert.IsNotNull(computerNames); - Assert.AreEqual(3, computerNames.Count); - CollectionAssert.AreEqual(new[] { "server1", "server2", "server3" }, computerNames.ToArray()); - } - - [TestMethod("5.4. 分号分隔的数组参数。")] - public void SemicolonSeparatedArrayParameter() - { - // Arrange - string[] args = ["-Processes", "chrome;firefox;edge"]; - string[]? processes = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => processes = o.Processes) - .Run(); - - // Assert - Assert.IsNotNull(processes); - Assert.AreEqual(3, processes.Length); - CollectionAssert.AreEqual(new[] { "chrome", "firefox", "edge" }, processes); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("5.6. 逗号分隔的带引号数组元素。")] - public void CommaSeparatedQuotedArrayElements() - { - // Arrange - string[] args = ["-ComputerNames", "\"server one\",\"server two\",server3"]; - string[]? computerNames = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => computerNames = o.ComputerNames) - .Run(); - - // Assert - Assert.IsNotNull(computerNames); - Assert.AreEqual(3, computerNames.Length); - CollectionAssert.AreEqual(new[] { "server one", "server two", "server3" }, computerNames); - } - - #endregion - - #region 6. 边界条件处理 - - [TestMethod("6.1. 必选参数缺失,抛出异常。")] - public void MissingRequiredParameter_ThrowsException() - { - // Arrange - string[] args = []; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, PowerShell) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.2. 类型转换错误,抛出异常。")] - public void TypeConversionError_ThrowsException() - { - // Arrange - string[] args = ["-Count", "not-a-number"]; - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, PowerShell) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("6.3. 参数大小写不敏感。")] - public void ParameterCaseInsensitive() - { - // Arrange - string[] args = ["-NAME", "test", "-path", "C:\\temp"]; - string? name = null; - string? path = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => - { - name = o.Name; - path = o.Path; - }) - .Run(); - - // Assert - Assert.AreEqual("test", name); - Assert.AreEqual("C:\\temp", path); - } - - #endregion - - #region 7. 特殊场景 - - [TestMethod("7.1. 引号包围的参数值。")] - public void QuotedParameterValues() - { - // Arrange - string[] args = ["-Message", "\"Hello World\""]; - string? message = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => message = o.Message) - .Run(); - - // Assert - Assert.AreEqual("\"Hello World\"", message); - } - - [TestMethod("7.2. 参数别名支持。")] - public void ParameterAliases() - { - // Arrange - string[] args = ["-Alias", "test"]; - string? value = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => value = o.ParameterWithAlias) - .Run(); - - // Assert - Assert.AreEqual("test", value); - } - - [TestMethod("7.3. 枚举类型参数。")] - public void EnumTypeParameter() - { - // Arrange - string[] args = ["-LogLevel", "Warning"]; - LogLevel? logLevel = null; - - // Act - CommandLine.Parse(args, PowerShell) - .AddHandler(o => logLevel = o.LogLevel) - .Run(); - - // Assert - Assert.AreEqual(LogLevel.Warning, logLevel); - } - - #endregion - - #region 8. 异步处理测试 - - [TestMethod("8.1. 异步处理方法,正确执行。")] - public async Task AsyncHandler_ExecutesCorrectly() - { - // Arrange - string[] args = ["-Name", "async-test"]; - string? name = null; - - // Act - await CommandLine.Parse(args, PowerShell) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - name = o.Name; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("async-test", name); - } - - #endregion -} - -#region 测试用数据模型 - -internal record PS01_BasicOptions -{ - [Option("Name")] - public string Name { get; init; } = string.Empty; -} - -internal record PS02_MultipleOptions -{ - [Option("Path")] - public string Path { get; init; } = string.Empty; - - [Option("ItemType")] - public string ItemType { get; init; } = string.Empty; - - [Option("Force")] - public bool Force { get; init; } -} - -internal record PS03_CamelCaseOptions -{ - [Option("fileName")] - public string fileName { get; init; } = string.Empty; -} - -internal record PS04_SwitchOptions -{ - [Option("Verbose")] - public bool Verbose { get; init; } -} - -internal record PS05_MultipleSwitchOptions -{ - [Option("Recurse")] - public bool Recurse { get; init; } - - [Option("Force")] - public bool Force { get; init; } - - [Option("WhatIf")] - public bool WhatIf { get; init; } -} - -internal record PS06_MixedParameterTypes -{ - [Option("Path")] - public string Path { get; init; } = string.Empty; - - [Option("Append")] - public bool Append { get; init; } - - [Option("Encoding")] - public string Encoding { get; init; } = string.Empty; -} - -internal record PS07_AbbreviationOptions -{ - [Option("ComputerName")] - public string ComputerName { get; init; } = string.Empty; -} - -internal record PS08_AmbiguousOptions -{ - [Option("ComputerName")] - public string ComputerName { get; init; } = string.Empty; - - [Option("Count")] - public int Count { get; init; } -} - -internal record PS09_PositionalOptions -{ - [Value] - public string Value { get; init; } = string.Empty; -} - -internal record PS10_MultiplePositionalOptions -{ - [Value(0)] - public string Source { get; init; } = string.Empty; - - [Value(1)] - public string Destination { get; init; } = string.Empty; -} - -internal record PS11_MixedParameterOptions -{ - [Value(0)] - public string Source { get; init; } = string.Empty; - - [Option("Destination")] - public string Destination { get; init; } = string.Empty; - - [Option("Force")] - public bool Force { get; init; } -} - -internal record PS12_ArrayOptions -{ - [Option("Processes")] - public string[] Processes { get; init; } = []; -} - -internal record PS13_RepeatedParameterOptions -{ - [Option("ComputerName")] - public IReadOnlyList ComputerName { get; init; } = []; -} - -internal record PS14_RequiredOptions -{ - [Option("Name")] - public required string Name { get; init; } -} - -internal record PS15_TypeConversionOptions -{ - [Option("Count")] - public int Count { get; init; } -} - -internal record PS16_CaseInsensitiveOptions -{ - [Option("Name")] - public string Name { get; init; } = string.Empty; - - [Option("Path")] - public string Path { get; init; } = string.Empty; -} - -internal record PS17_QuotedValueOptions -{ - [Option("Message")] - public string Message { get; init; } = string.Empty; -} - -internal record PS18_AliasOptions -{ - [Option([], ["ParameterWithAlias", "Alias", "Alt"])] - public string ParameterWithAlias { get; init; } = string.Empty; -} - -internal record PS19_EnumOptions -{ - [Option("LogLevel")] - public LogLevel LogLevel { get; init; } -} - -internal record PS19_ArrayMultiValueOptions -{ - [Option("Tags")] - public string[] Tags { get; init; } = []; -} - -internal record PS20_QuotedArrayOptions -{ - [Option("Files")] - public string[] Files { get; init; } = []; - - [Option("ComputerNames")] - public string[] ComputerNames { get; init; } = []; -} - -#endregion diff --git a/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs deleted file mode 100644 index a94921c2..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/SubcommandTests.cs +++ /dev/null @@ -1,900 +0,0 @@ -using System.Threading.Tasks; -using DotNetCampus.Cli.Compiler; -using DotNetCampus.Cli.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable InconsistentNaming - -namespace DotNetCampus.Cli.Tests; - -/// -/// 测试子命令(SubCommand)功能,包括二级子命令、多级子命令和嵌套子命令。 -/// -[TestClass] -public class SubcommandTests -{ - private CommandLineParsingOptions Flexible { get; } = CommandLineParsingOptions.Flexible; - - #region 1. 基本子命令测试 - - [TestMethod("1.1. 二级子命令匹配")] - public void BasicSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["remote", "add", "origin", "https://github.com/user/repo.git"]; - string? capturedRemoteName = null; - string? capturedRemoteUrl = null; - bool otherHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedRemoteName = o.RemoteName; - capturedRemoteUrl = o.RemoteUrl; - }) - .AddHandler(_ => otherHandlerCalled = true) - .Run(); - - // Assert - Assert.AreEqual("origin", capturedRemoteName); - Assert.AreEqual("https://github.com/user/repo.git", capturedRemoteUrl); - Assert.IsFalse(otherHandlerCalled); - } - - [TestMethod("1.2. 另一个二级子命令匹配")] - public void AnotherBasicSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["container", "run", "--name", "test-container", "nginx"]; - string? capturedContainerName = null; - string? capturedImageName = null; - bool otherHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedContainerName = o.ContainerName; - capturedImageName = o.ImageName; - }) - .AddHandler(_ => otherHandlerCalled = true) - .Run(); - - // Assert - Assert.AreEqual("test-container", capturedContainerName); - Assert.AreEqual("nginx", capturedImageName); - Assert.IsFalse(otherHandlerCalled); - } - - [TestMethod("1.3. 单级命令与二级子命令共存")] - public void SingleCommandAndSubcommand_Coexist() - { - // Arrange - 测试主命令 - string[] mainArgs = ["status"]; - bool statusHandlerCalled = false; - bool subcommandHandlerCalled = false; - - // Act - 执行主命令 - CommandLine.Parse(mainArgs, Flexible) - .AddHandler(_ => statusHandlerCalled = true) - .AddHandler(_ => subcommandHandlerCalled = true) - .Run(); - - // Assert - Assert.IsTrue(statusHandlerCalled); - Assert.IsFalse(subcommandHandlerCalled); - - // Reset - statusHandlerCalled = false; - subcommandHandlerCalled = false; - - // Arrange - 测试子命令 - string[] subArgs = ["remote", "add", "origin", "https://example.com"]; - - // Act - 执行子命令 - CommandLine.Parse(subArgs, Flexible) - .AddHandler(_ => statusHandlerCalled = true) - .AddHandler(_ => subcommandHandlerCalled = true) - .Run(); - - // Assert - Assert.IsFalse(statusHandlerCalled); - Assert.IsTrue(subcommandHandlerCalled); - } - - #endregion - - #region 2. 多级子命令测试 - - [TestMethod("2.1. 三级子命令匹配")] - public void ThreeLevelSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["container", "image", "list"]; - bool handlerCalled = false; - bool otherHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => handlerCalled = true) - .AddHandler(_ => otherHandlerCalled = true) - .Run(); - - // Assert - Assert.IsTrue(handlerCalled); - Assert.IsFalse(otherHandlerCalled); - } - - [TestMethod("2.2. 另一个三级子命令匹配")] - public void AnotherThreeLevelSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["cluster", "node", "delete", "worker-node-1"]; - string? capturedNodeName = null; - bool otherHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedNodeName = o.NodeName; - }) - .AddHandler(_ => otherHandlerCalled = true) - .Run(); - - // Assert - Assert.AreEqual("worker-node-1", capturedNodeName); - Assert.IsFalse(otherHandlerCalled); - } - - [TestMethod("2.3. 四级子命令匹配")] - public void FourLevelSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["config", "user", "profile", "set", "development"]; - string? capturedProfile = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedProfile = o.ProfileName; - }) - .Run(); - - // Assert - Assert.AreEqual("development", capturedProfile); - } - - [TestMethod("2.4. kebab-case 命名的子命令匹配")] - public void KebabCaseSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["get-info", "user", "123"]; - string? capturedUserId = null; - bool otherHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedUserId = o.UserId; - }) - .AddHandler(_ => otherHandlerCalled = true) - .Run(); - - // Assert - Assert.AreEqual("123", capturedUserId); - Assert.IsFalse(otherHandlerCalled); - } - - [TestMethod("2.5. 混合 kebab-case 和普通命名的多级子命令")] - public void MixedKebabCaseAndNormalSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["user-management", "create-account", "--username", "john", "--email", "john@example.com"]; - string? capturedUsername = null; - string? capturedEmail = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedUsername = o.Username; - capturedEmail = o.Email; - }) - .Run(); - - // Assert - Assert.AreEqual("john", capturedUsername); - Assert.AreEqual("john@example.com", capturedEmail); - } - - [TestMethod("2.6. 复杂的 kebab-case 三级子命令")] - public void ComplexKebabCaseThreeLevelSubcommand_MatchesCorrectly() - { - // Arrange - string[] args = ["cloud-service", "auto-scaling", "set-policy", "scale-up"]; - string? capturedPolicy = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedPolicy = o.PolicyName; - }) - .Run(); - - // Assert - Assert.AreEqual("scale-up", capturedPolicy); - } - - #endregion - - #region 3. 子命令优先级与匹配规则测试 - - [TestMethod("3.1. 更具体的子命令优先匹配")] - public void MoreSpecificSubcommand_TakesPriority() - { - // Arrange - string[] args = ["remote", "add", "origin", "https://example.com"]; - bool genericRemoteHandlerCalled = false; - bool specificRemoteAddHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => genericRemoteHandlerCalled = true) - .AddHandler(_ => specificRemoteAddHandlerCalled = true) - .Run(); - - // Assert - Assert.IsFalse(genericRemoteHandlerCalled); - Assert.IsTrue(specificRemoteAddHandlerCalled); - } - - [TestMethod("3.2. 部分匹配子命令的处理")] - public void PartialSubcommandMatch_MatchesLongestPath() - { - // Arrange - string[] args = ["container", "run", "nginx"]; - bool containerHandlerCalled = false; - bool containerRunHandlerCalled = false; - bool containerImageHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => containerHandlerCalled = true) - .AddHandler(_ => containerRunHandlerCalled = true) - .AddHandler(_ => containerImageHandlerCalled = true) - .Run(); - - // Assert - Assert.IsFalse(containerHandlerCalled); - Assert.IsTrue(containerRunHandlerCalled); - Assert.IsFalse(containerImageHandlerCalled); - } - - [TestMethod("3.3. 注册顺序不影响子命令匹配优先级")] - public void RegistrationOrder_DoesNotAffectSubcommandPriority() - { - // Arrange - string[] args = ["remote", "add", "origin", "https://example.com"]; - bool genericRemoteHandlerCalled = false; - bool specificRemoteAddHandlerCalled = false; - - // Act - 先注册具体的,再注册通用的 - CommandLine.Parse(args, Flexible) - .AddHandler(_ => specificRemoteAddHandlerCalled = true) - .AddHandler(_ => genericRemoteHandlerCalled = true) - .Run(); - - // Assert - Assert.IsFalse(genericRemoteHandlerCalled); - Assert.IsTrue(specificRemoteAddHandlerCalled); - } - - [TestMethod("3.4. 最长路径匹配 - 基本情况")] - public void LongestPathMatching_BasicCase() - { - // Arrange - 测试 "git", "git remote", "git remote add" 的优先级 - string[] args = ["git", "remote", "add", "origin", "https://example.com"]; - bool gitHandlerCalled = false; - bool gitRemoteHandlerCalled = false; - bool gitRemoteAddHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => gitHandlerCalled = true) - .AddHandler(_ => gitRemoteHandlerCalled = true) - .AddHandler(_ => gitRemoteAddHandlerCalled = true) - .Run(); - - // Assert - 应该匹配最长的 "git remote add" - Assert.IsFalse(gitHandlerCalled); - Assert.IsFalse(gitRemoteHandlerCalled); - Assert.IsTrue(gitRemoteAddHandlerCalled); - } - - [TestMethod("3.5. 最长路径匹配 - 复杂情况")] - public void LongestPathMatching_ComplexCase() - { - // Arrange - 测试多个不同长度的命令路径 - string[] args = ["cluster", "config", "set-context", "my-context"]; - bool clusterHandlerCalled = false; - bool clusterConfigHandlerCalled = false; - bool clusterConfigSetHandlerCalled = false; - bool clusterConfigSetContextHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => clusterHandlerCalled = true) - .AddHandler(_ => clusterConfigHandlerCalled = true) - .AddHandler(_ => clusterConfigSetHandlerCalled = true) - .AddHandler(_ => clusterConfigSetContextHandlerCalled = true) - .Run(); - - // Assert - 应该匹配最长的 "cluster config set-context" - Assert.IsFalse(clusterHandlerCalled); - Assert.IsFalse(clusterConfigHandlerCalled); - Assert.IsFalse(clusterConfigSetHandlerCalled); - Assert.IsTrue(clusterConfigSetContextHandlerCalled); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.6. 最长路径匹配 - 前缀匹配但非完整匹配")] - public void LongestPathMatching_PrefixButNotComplete() - { - // Arrange - "remote addx" 不应该匹配 "remote add" - string[] args = ["remote", "addx", "test"]; - bool remoteHandlerCalled = false; - bool remoteAddHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => remoteHandlerCalled = true) - .AddHandler(_ => remoteAddHandlerCalled = true) - .Run(); - - // Assert - addx 不能匹配 add,所以只有 remote 是匹配的 - Assert.IsTrue(remoteHandlerCalled); - Assert.IsFalse(remoteAddHandlerCalled); - } - - [TestMethod("3.7. 最长路径匹配 - 大小写不敏感")] - public void LongestPathMatching_CaseInsensitive() - { - // Arrange - string[] args = ["Remote", "ADD", "origin", "https://example.com"]; - bool remoteHandlerCalled = false; - bool remoteAddHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) // Flexible 默认大小写不敏感 - .AddHandler(_ => remoteHandlerCalled = true) - .AddHandler(_ => remoteAddHandlerCalled = true) - .Run(); - - // Assert - Assert.IsFalse(remoteHandlerCalled); - Assert.IsTrue(remoteAddHandlerCalled); - } - - [Ignore("规范行为后,此测试不再适用。")] - [TestMethod("3.8. 最长路径匹配 - 单个字符差异")] - public void LongestPathMatching_SingleCharacterDifference() - { - // Arrange - 测试命令名称相似但不同的情况 - string[] args = ["config", "users", "list"]; - bool configUserHandlerCalled = false; - bool configUsersHandlerCalled = false; - bool configUserListHandlerCalled = false; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(_ => configUserHandlerCalled = true) - .AddHandler(_ => configUsersHandlerCalled = true) - .AddHandler(_ => configUserListHandlerCalled = true) - .Run(); - - // Assert - 应该匹配 "config users" 而不是其他 - Assert.IsFalse(configUserHandlerCalled); - Assert.IsTrue(configUsersHandlerCalled); - Assert.IsFalse(configUserListHandlerCalled); - } - - #endregion - - #region 4. 子命令参数与选项测试 - - [TestMethod("4.1. 子命令带选项参数")] - public void Subcommand_WithOptions_ParsedCorrectly() - { - // Arrange - string[] args = ["remote", "add", "origin", "https://example.com", "--fetch", "--tags"]; - bool fetchEnabled = false; - bool tagsEnabled = false; - string? remoteName = null; - string? remoteUrl = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - fetchEnabled = o.EnableFetch; - tagsEnabled = o.EnableTags; - remoteName = o.RemoteName; - remoteUrl = o.RemoteUrl; - }) - .Run(); - - // Assert - Assert.IsTrue(fetchEnabled); - Assert.IsTrue(tagsEnabled); - Assert.AreEqual("origin", remoteName); - Assert.AreEqual("https://example.com", remoteUrl); - } - - [TestMethod("4.2. 子命令带位置参数和选项混合")] - public void Subcommand_WithMixedPositionalAndOptions_ParsedCorrectly() - { - // Arrange - string[] args = ["container", "run", "--detach", "--publish", "8080:80", "nginx", "nginx:latest"]; - bool detached = false; - string? portMapping = null; - string? containerName = null; - string? imageName = null; - - // Act - CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - detached = o.Detach; - portMapping = o.Publish; - containerName = o.ContainerName; - imageName = o.ImageName; - }) - .Run(); - - // Assert - Assert.IsTrue(detached); - Assert.AreEqual("8080:80", portMapping); - Assert.AreEqual("nginx", containerName); - Assert.AreEqual("nginx:latest", imageName); - } - - #endregion - - #region 5. 子命令错误处理测试 - - [TestMethod("5.1. 未知子命令抛出异常")] - public void UnknownSubcommand_ThrowsCommandNameNotFoundException() - { - // Arrange - string[] args = ["unknown", "subcommand"]; - - // Act & Assert - var exception = Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .AddHandler(_ => { }) - .Run(); - }); - - // 确认异常包含正确的子命令信息 - Assert.IsTrue(exception.Message.Contains("unknown")); - } - - [TestMethod("5.2. 子命令缺少必需参数抛出异常")] - public void Subcommand_MissingRequiredParameter_ThrowsException() - { - // Arrange - string[] args = ["remote", "add"]; // 缺少 remote name 和 URL - - // Act & Assert - Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .Run(); - }); - } - - [TestMethod("5.3. 部分匹配但无完全匹配的子命令")] - public void PartialSubcommandMatch_NoExactMatch_ThrowsException() - { - // Arrange - "remote" 存在,但 "remote unknown" 不存在 - string[] args = ["remote", "unknown"]; - - // Act & Assert - var exception = Assert.ThrowsExactly(() => - { - CommandLine.Parse(args, Flexible) - .AddHandler(_ => { }) - .AddHandler(_ => { }) - .Run(); - }); - - Assert.IsTrue(exception.Message.Contains("remote")); - } - - #endregion - - #region 6. 异步子命令处理测试 - - [TestMethod("6.1. 异步子命令处理")] - public async Task AsyncSubcommand_ExecutesSuccessfully() - { - // Arrange - string[] args = ["remote", "sync", "origin"]; - string? capturedRemoteName = null; - bool asyncOperationCompleted = false; - - // Act - await CommandLine.Parse(args, Flexible) - .AddHandler(async o => - { - await Task.Delay(10); // 模拟异步操作 - capturedRemoteName = o.RemoteName; - asyncOperationCompleted = true; - return 0; - }) - .RunAsync(); - - // Assert - Assert.AreEqual("origin", capturedRemoteName); - Assert.IsTrue(asyncOperationCompleted); - } - - [TestMethod("6.2. 混合同步异步子命令处理")] - public async Task MixedSyncAsyncSubcommands_ExecuteCorrectly() - { - // Arrange - string[] args = ["container", "build", ".", "--tag", "myapp"]; - string? capturedTag = null; - string? capturedPath = null; - bool otherHandlerCalled = false; - - // Act - await CommandLine.Parse(args, Flexible) - .AddHandler(o => - { - capturedPath = o.BuildPath; - capturedTag = o.Tag; - }) - .AddHandler(_ => Task.FromResult(otherHandlerCalled = true)) - .RunAsync(); - - // Assert - Assert.AreEqual(".", capturedPath); - Assert.AreEqual("myapp", capturedTag); - Assert.IsFalse(otherHandlerCalled); - } - - #endregion - - #region 7. ICommandHandler 接口子命令测试 - - [TestMethod("7.1. ICommandHandler 接口实现的子命令")] - public async Task ICommandHandler_Subcommand_ExecutesCorrectly() - { - // Arrange - string[] args = ["service", "start", "web-api"]; - - // Act - int exitCode = await CommandLine.Parse(args, Flexible) - .AddHandler() - .RunAsync(); - - // Assert - Assert.AreEqual(ServiceStartCommandHandler.ExpectedExitCode, exitCode); - Assert.IsTrue(ServiceStartCommandHandler.WasHandlerCalled); - Assert.AreEqual("web-api", ServiceStartCommandHandler.CapturedServiceName); - - // Reset static state for other tests - ServiceStartCommandHandler.ResetState(); - } - - #endregion -} - -#region 测试用数据模型 - -// Git 相关子命令选项类 - -[Command("status")] -internal class GitStatusOptions -{ - [Option("short")] - public bool Short { get; init; } -} - -[Command("remote")] -internal class GitRemoteOptions -{ - [Option("verbose")] - public bool Verbose { get; init; } -} - -[Command("remote add")] -internal class GitRemoteAddOptions -{ - [Value(0)] - public required string RemoteName { get; init; } - - [Value(1)] - public required string RemoteUrl { get; init; } -} - -[Command("remote add")] -internal class GitRemoteNullableAddOptions -{ - [Value(0)] - public string? RemoteName { get; init; } - - [Value(1)] - public string? RemoteUrl { get; init; } -} - -[Command("remote list")] -internal class GitRemoteListOptions -{ - [Option("verbose")] - public bool Verbose { get; init; } -} - -[Command("remote add")] -internal class GitRemoteAddOptionsWithFlags -{ - [Option("fetch")] - public bool EnableFetch { get; init; } - - [Option("tags")] - public bool EnableTags { get; init; } - - [Value(0)] - public required string RemoteName { get; init; } - - [Value(1)] - public required string RemoteUrl { get; init; } -} - -[Command("remote sync")] -internal class GitRemoteSyncOptions -{ - [Value(0)] - public required string RemoteName { get; init; } -} - -// Docker 相关子命令选项类 - -[Command("container run")] -internal class DockerContainerRunOptions -{ - [Option("name")] - public string? ContainerName { get; init; } - - [Value(0)] - public required string ImageName { get; init; } -} - -[Command("container list")] -internal class DockerContainerListOptions -{ - [Option("all")] - public bool ShowAll { get; init; } -} - -[Command("container")] -internal class DockerContainerOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("container image")] -internal class DockerContainerImageOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("container image list")] -internal class DockerContainerImageListOptions -{ - [Option("all")] - public bool ShowAll { get; init; } -} - -[Command("container run")] -internal class DockerContainerRunOptionsDetailed -{ - [Option("detach")] - public bool Detach { get; init; } - - [Option("publish")] - public string? Publish { get; init; } - - [Value(0)] - public required string ContainerName { get; init; } - - [Value(1)] - public required string ImageName { get; init; } -} - -[Command("container build")] -internal class DockerContainerBuildOptions -{ - [Value(0)] - public required string BuildPath { get; init; } - - [Option("tag")] - public string? Tag { get; init; } -} - -// Kubernetes 相关子命令选项类 - -[Command("cluster node delete")] -internal class KubernetesClusterNodeDeleteOptions -{ - [Value(0)] - public required string NodeName { get; init; } -} - -// 配置管理相关子命令选项类 - -[Command("config user profile set")] -internal class ConfigUserProfileSetOptions -{ - [Value(0)] - public required string ProfileName { get; init; } -} - -// 服务管理相关子命令选项类 - -[Command("service start")] -internal class ServiceStartCommandHandler : ICommandHandler -{ - public static bool WasHandlerCalled { get; private set; } - public static string? CapturedServiceName { get; private set; } - public const int ExpectedExitCode = 100; - - [Value(0)] - public required string ServiceName { get; init; } - - public Task RunAsync() - { - WasHandlerCalled = true; - CapturedServiceName = ServiceName; - return Task.FromResult(ExpectedExitCode); - } - - public static void ResetState() - { - WasHandlerCalled = false; - CapturedServiceName = null; - } -} - -// kebab-case 命名相关子命令选项类 - -[Command("get-info user")] -internal class GetInfoUserOptions -{ - [Value(0)] - public required string UserId { get; init; } -} - -[Command("get-info system")] -internal class GetInfoSystemOptions -{ - [Option("verbose")] - public bool Verbose { get; init; } -} - -[Command("user-management create-account")] -internal class UserManagementCreateAccountOptions -{ - [Option("username")] - public required string Username { get; init; } - - [Option("email")] - public required string Email { get; init; } - - [Option("role")] - public string Role { get; init; } = "user"; -} - -[Command("cloud-service auto-scaling set-policy")] -internal class CloudServiceAutoScalingSetPolicyOptions -{ - [Value(0)] - public required string PolicyName { get; init; } - - [Option("min-instances")] - public int MinInstances { get; init; } = 1; - - [Option("max-instances")] - public int MaxInstances { get; init; } = 10; -} - -// 新增的测试数据模型类 - 用于最长路径匹配测试 - -[Command("git")] -internal class GitBaseOptions -{ - [Option("version")] - public bool ShowVersion { get; init; } -} - -[Command("cluster")] -internal class KubernetesClusterOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("cluster config")] -internal class KubernetesClusterConfigOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("cluster config set")] -internal class KubernetesClusterConfigSetOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("cluster config set-context")] -internal class KubernetesClusterConfigSetContextOptions -{ - [Value(0)] - public required string ContextName { get; init; } -} - -[Command("config user")] -internal class ConfigUserOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("config users")] -internal class ConfigUsersOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -[Command("config user list")] -internal class ConfigUserListOptions -{ - [Option("help")] - public bool ShowHelp { get; init; } -} - -// 新增用于最长路径匹配测试的 Git 命令类 - -[Command("git remote")] -internal class GitRemoteOptionsNew -{ - [Option("verbose")] - public bool Verbose { get; init; } -} - -[Command("git remote add")] -internal class GitRemoteAddOptionsNew -{ - [Value(0)] - public required string RemoteName { get; init; } - - [Value(1)] - public required string RemoteUrl { get; init; } -} - -#endregion From 366b7f80741393706ce84424480c7e0d5e81470c Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 13:07:52 +0800 Subject: [PATCH 112/193] =?UTF-8?q?=E5=88=A0=E9=99=A4=203.x=20=E6=97=B6?= =?UTF-8?q?=E4=BB=A3=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ionLongNameMustBePascalCaseAnalyzerTest.cs | 99 ----- .../CommandLineTests.ValueRange.cs | 62 --- .../CommandLineTests.bak.cs | 419 ------------------ .../DotNetCampus.CommandLine.Tests.csproj | 8 - 4 files changed, 588 deletions(-) delete mode 100644 tests/DotNetCampus.CommandLine.Tests/Analyzers/OptionLongNameMustBePascalCaseAnalyzerTest.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/CommandLineTests.ValueRange.cs delete mode 100644 tests/DotNetCampus.CommandLine.Tests/CommandLineTests.bak.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/Analyzers/OptionLongNameMustBePascalCaseAnalyzerTest.cs b/tests/DotNetCampus.CommandLine.Tests/Analyzers/OptionLongNameMustBePascalCaseAnalyzerTest.cs deleted file mode 100644 index 6d275233..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/Analyzers/OptionLongNameMustBePascalCaseAnalyzerTest.cs +++ /dev/null @@ -1,99 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; -//using System.Threading.Tasks; -//using System.Xml; - -//using DotNetCampus.CommandLine; -//using DotNetCampus.CommandLine.Analyzers; - -//using Microsoft.CodeAnalysis; -//using Microsoft.CodeAnalysis.Diagnostics; -//using Microsoft.VisualStudio.TestTools.UnitTesting; - -//using MSTest.Extensions.Contracts; - -//using RoslynTestKit; - -//namespace DotNetCampus.Cli.Tests.Analyzers -//{ -// [TestClass] -// public class OptionLongNameMustBePascalCaseAnalyzerTest : AnalyzerTestFixture -// { -// protected override string LanguageName => LanguageNames.CSharp; -// protected override DiagnosticAnalyzer CreateAnalyzer() => new OptionLongNameMustBePascalCaseAnalyzer(); - -// [ContractTestCase] -// public void TestWithoutNumbers() -// { -// "使用 Pascal 风格的长名称,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( -// "WalterlvIsAdobe", -// "Walterlv"); - -// "使用非 Pascal 风格的长名称,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( -// "--walterlv-is-adobe", -// "-WalterlvIsAdobe", -// "/WalterlvIsAdobe", -// "walterlv-is-adobe", -// "walterlvIsAdobe", -// "walterlv_is_adobe", -// "waltelv", -// "--walterlv", -// "-Walterlv", -// "/Walterlv"); - -// "多位全大写字母,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( -// "HTML", -// "AddedHTMLFile"); - -// "两位全大写字母,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( -// "IO", -// "IOSetting", -// "TestIO", -// "TestIOSetting"); -// } - -// [ContractTestCase] -// public void TestWithNumbers() -// { -// "使用 Pascal 风格的长名称,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( -// "Files2Build", -// "Html5"); - -// "使用非 Pascal 风格的长名称,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( -// "--walterlv-is-adobe", -// "-WalterlvIsAdobe", -// "/WalterlvIsAdobe", -// "walterlv-is-adobe", -// "walterlvIsAdobe", -// "walterlv_is_adobe", -// "waltelv", -// "--walterlv", -// "-Walterlv", -// "/Walterlv"); -// } - -// private void TestHasDiagnostic(string longName) -// { -// string code = $@" -//class Options -//{{ -// [Option('d', ""{longName}"")] -// public string? DemoOption {{ get; set; }} -//}}"; -// HasDiagnostic(code, DiagnosticIds.OptionLongNameMustBePascalCase); -// } - -// private void TestNoDiagnostic(string longName) -// { -// string code = $@" -//class Options -//{{ -// [Option('d', ""{longName}"")] -// public string? DemoOption {{ get; set; }} -//}}"; -// NoDiagnostic(code, DiagnosticIds.OptionLongNameMustBePascalCase); -// } -// } -//} diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.ValueRange.cs b/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.ValueRange.cs deleted file mode 100644 index e5c56468..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.ValueRange.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections; - -using DotNetCampus.Cli.Tests.Fakes; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using MSTest.Extensions.Contracts; - -namespace DotNetCampus.Cli.Tests -{ - public partial class CommandLineTests - { - [ContractTestCase] - public void ParseValues() - { - "命令行中包含 --,那么 -- 后的字符串完全属于值。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("foo", options.Foo); - Assert.AreEqual(8, options.LongValue); - CollectionAssert.AreEqual(new[] { "x", "y" }, (ICollection?)options.Values); - Assert.AreEqual(2, options.Int32Value); - }).WithArguments( - new[] { "8", "x", "y", "2", "-f", "foo" }, - new[] { "-f", "foo", "--", "8", "x", "y", "2" } - ); - - "命令行中包含 --,那么 -- 后的字符串完全属于值,即使后面包含 -。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("foo", options.Foo); - Assert.AreEqual(-8, options.LongValue); - CollectionAssert.AreEqual(new[] { "-x", "-y" }, (ICollection?)options.Values); - Assert.AreEqual(-2, options.Int32Value); - }).WithArguments( - new[] { "-f", "foo", "--", "-8", "-x", "-y", "-2" } - ); - - "命令行中包含 --,那么 -- 后的字符串完全属于值,且完全赋值。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("foo", options.Section); - Assert.AreEqual(8, options.Count); - CollectionAssert.AreEqual(new[] { "dcl.exe", "--foo", "xyz", "-s", "some", "2" }, (ICollection?)options.Args); - }).WithArguments( - new[] { "-s", "foo", "--", "8", "dcl.exe", "--foo", "xyz", "-s", "some", "2" } - ); - } - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.bak.cs b/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.bak.cs deleted file mode 100644 index 43803c1c..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/CommandLineTests.bak.cs +++ /dev/null @@ -1,419 +0,0 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using DotNetCampus.Cli.Tests.Fakes; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using MSTest.Extensions.Contracts; - -using static DotNetCampus.Cli.Tests.Fakes.CommandLineArgs; - -namespace DotNetCampus.Cli.Tests -{ - [TestClass] - public partial class CommandLineTests - { - [ContractTestCase] - public void ParseAs() - { - "命令行中没有参数,正确完成解析。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(NoArgs); - var options = commandLine.As(new OptionsParser()); - - // Assert - Assert.AreEqual(null, options.FilePath); - Assert.AreEqual(false, options.IsFromCloud); - Assert.AreEqual(false, options.IsIwb); - Assert.AreEqual(null, options.StartupMode); - Assert.AreEqual(false, options.IsSilence); - Assert.AreEqual(null, options.Placement); - Assert.AreEqual(null, options.StartupSession); - }); - - "使用 {0} 风格的命令行,正确完成解析。".Test((string name, string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args, protocolName: UrlProtocol); - var options = commandLine.As(new OptionsParser()); - - // Assert - Assert.AreEqual(FileValue, options.FilePath); - Assert.AreEqual(CloudValue, options.IsFromCloud); - Assert.AreEqual(IwbValue, options.IsIwb); - Assert.AreEqual(ModeValue, options.StartupMode); - Assert.AreEqual(SilenceValue, options.IsSilence); - Assert.AreEqual(PlacementValue, options.Placement); - Assert.AreEqual(StartupSessionValue, options.StartupSession); - }).WithArguments( - ("Windows", WindowsStyleArgs), - ("Cmd", CmdStyleArgs), - ("Cmd2", Cmd2StyleArgs), - ("Linux", LinuxStyleArgs), - ("Url", UrlArgs)); - - "使用运行时解析器解析至可变类型,正确完成解析。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(WindowsStyleArgs); - var options = commandLine.As(); - - // Assert - Assert.AreEqual(FileValue, options.FilePath); - Assert.AreEqual(CloudValue, options.IsFromCloud); - Assert.AreEqual(IwbValue, options.IsIwb); - Assert.AreEqual(ModeValue, options.StartupMode); - Assert.AreEqual(SilenceValue, options.IsSilence); - Assert.AreEqual(PlacementValue, options.Placement); - Assert.AreEqual(StartupSessionValue, options.StartupSession); - }); - - "使用运行时解析器解析至不可变类型,正确完成解析。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(WindowsStyleArgs); - var options = commandLine.As(); - - // Assert - Assert.AreEqual(FileValue, options.FilePath); - Assert.AreEqual(CloudValue, options.IsFromCloud); - Assert.AreEqual(IwbValue, options.IsIwb); - Assert.AreEqual(ModeValue, options.StartupMode); - Assert.AreEqual(SilenceValue, options.IsSilence); - Assert.AreEqual(PlacementValue, options.Placement); - Assert.AreEqual(StartupSessionValue, options.StartupSession); - }); - } - - [ContractTestCase] - public void ParseToPrimary() - { - "命令行传入数值,可以解析为数值类型。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual((byte)1, options.Aaa); - Assert.AreEqual((short)2, options.Bbb); - Assert.AreEqual((ushort)3, options.Ccc); - Assert.AreEqual((int)4, options.Ddd); - Assert.AreEqual((uint)5, options.Eee); - Assert.AreEqual((long)6, options.Fff); - Assert.AreEqual((ulong)7, options.Ggg); - Assert.AreEqual((float)8, options.Hhh); - Assert.AreEqual((double)9, options.Iii); - Assert.AreEqual((decimal)10, options.Jjj); - }).WithArguments( - new[] { "-a", "1", "-b", "2", "-c", "3", "-d", "4", "-e", "5", "-f", "6", "-g", "7", "-h", "8", "-i", "9", "-j", "10" }, - new[] { "-a", "1", "-b", "2", "-c", "3", "-d", "4", "-e", "5", "-f", "6", "-g", "7", "-h", "8.0", "-i", "9.0", "-j", "10.0" } - ); - } - - [ContractTestCase] - public void ParseToIO() - { - "命令行传入文件路径,可以解析为文件路径类型。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual(Path.Combine(Directory.GetCurrentDirectory(), "a.txt"), options.File!.FullName); - Assert.AreEqual(Path.Combine(Directory.GetCurrentDirectory(), "b"), options.Directory!.FullName); - }).WithArguments( - new[] { "-f", "a.txt", "-d", "b" }, - new[] { "-f", " a.txt ", "-d", " b " } - ); - } - - [ContractTestCase] - public void ParseToDictionary() - { - "命令行传入字典(一项),能接收到字典的所有值。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("1", options.Aaa!["a"]); - Assert.AreEqual("1", options.Bbb!["a"]); - Assert.AreEqual("1", options.Ccc!["a"]); - Assert.AreEqual("a", options.Ddd.Key); - Assert.AreEqual("1", options.Ddd.Value); - }).WithArguments( - new[] { "-a", "a=1", "-b", "a=1", "-c", "a=1", "-d", "a=1" }, - new[] { "-a:a=1", "-b:a=1", "-c:a=1", "-d:a=1" } - ); - - "命令行传入字典(三项),能接收到字典的所有值。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("1", options.Aaa!["a"]); - Assert.AreEqual("2", options.Aaa["b"]); - Assert.AreEqual("3", options.Aaa["c"]); - Assert.AreEqual("1", options.Bbb!["a"]); - Assert.AreEqual("2", options.Bbb["b"]); - Assert.AreEqual("3", options.Bbb["c"]); - Assert.AreEqual("1", options.Ccc!["a"]); - Assert.AreEqual("2", options.Ccc["b"]); - Assert.AreEqual("3", options.Ccc["c"]); - }).WithArguments( - new[] { "-a", "a=1;b=2;c=3", "-b", "a=1;b=2;c=3", "-c", "a=1;b=2;c=3" }, - new[] { "-a:a=1;b=2;c=3", "-b:a=1;b=2;c=3", "-c:a=1;b=2;c=3" } - ); - - "命令行传入字典,能正确处理参数中的空格。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(); - - // Assert - Assert.AreEqual("1", options.Aaa!["a"]); - Assert.AreEqual("1 1", options.Bbb!["a"]); - Assert.AreEqual("1", options.Ccc!["a"]); - Assert.AreEqual("a", options.Ddd.Key); - Assert.AreEqual("1", options.Ddd.Value); - }).WithArguments( - new[] { "-a", "a = 1", "-b", "a=1 1", "-c", " a=1 ", "-d", "a =1" }, - new[] { "-a:a = 1", "-b:a=1 1", "-c: a=1 ", "-d:a =1" } - ); - } - - [ContractTestCase] - public void ParseAsAmbiguously() - { - "命令行传入开关参数,或者传入带有 true/false 值的参数,可以赋值给 bool 类型。".Test((string[] args) => - { - // Arrange & Action - var commandLine = CommandLine.Parse(args); - var options = commandLine.As(new AmbiguousOptionsParser()); - - // Assert - Assert.AreEqual(true, options.Boolean); - }).WithArguments( - new[] { "--boolean" }, - new[] { "--boolean", "true" } - ); - - "命令行传入带有 true/false 值的参数,可以赋值给 string 类型。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(new[] { "--string-boolean", "true" }); - var options = commandLine.As(new AmbiguousOptionsParser()); - - // Assert - Assert.AreEqual("true", options.StringBoolean); - }); - - "命令行传入带有多个值的参数,可以赋值给 string 类型。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(new[] { "--string-array", "a", "b" }); - var options = commandLine.As(new AmbiguousOptionsParser()); - - // Assert - Assert.AreEqual("a b", options.StringArray); - }); - - "命令行传入带有多个值的参数,可以赋值给 string 集合类型。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(new[] - { - "--array", "a", "b", - "--list", "a", "b", - "--read-only-list", "a", "b", - "--enumerable", "a", "b", - }); - var options = commandLine.As(); - - // Assert - CollectionAssert.AreEqual(new[] { "a", "b" }, options.Array); - CollectionAssert.AreEqual(new[] { "a", "b" }, options.List.ToArray()); - CollectionAssert.AreEqual(new[] { "a", "b" }, options.ReadOnlyList.ToArray()); - CollectionAssert.AreEqual(new[] { "a", "b" }, options.Enumerable.ToArray()); - }); - - "命令行传入带有多个值的参数,可以赋值给未内置的 string 集合类型。".Test(() => - { - // Arrange & Action - var commandLine = CommandLine.Parse(new[] - { - "--collection", "a", "b", - }); - var options = commandLine.As(); - - // Assert - CollectionAssert.AreEqual(new[] { "a", "b" }, options.Collection.ToArray()); - }); - } - - [ContractTestCase] - public void Handle() - { - const string expectedFilePath = @"C:\Users\lvyi\Test.txt"; - - "处理带有谓词的命令行参数,可以正确根据谓词选择处理函数。".Test((string[] args, int expectedExitCode) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - var exitCode = commandLine - .AddHandler(options => 0) - .AddHandler(options => 1) - .Run(); - - // Assert - Assert.AreEqual(expectedExitCode, exitCode); - }).WithArguments( - // 不区分大小写。 - (new[] { "Edit", expectedFilePath }, 0), - (new[] { "edit", expectedFilePath }, 0), - (new[] { "Print", expectedFilePath }, 1)); - - "处理带有谓词的命令行参数,可以正确解析出含谓词的命令行参数。".Test((string[] args) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - string? filePath = null; - commandLine - .AddHandler(options => filePath = options.FilePath) - .AddHandler(options => filePath = options.FilePath) - .AddHandler(options => { }) - .Run(); - - // Assert - Assert.AreEqual(expectedFilePath, filePath); - }).WithArguments( - // 不区分大小写。 - new[] { "Edit", expectedFilePath }, - new[] { "Print", expectedFilePath }); - - "处理带有默认谓词的命令行参数,可以在没有谓词的情况下解析。".Test((string[] args) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - string? filePath = null; - commandLine - .AddHandler(options => filePath = options.FilePath) - .AddHandler(options => filePath = options.FilePath) - .Run(); - - // Assert - Assert.AreEqual(expectedFilePath, filePath); - }).WithArguments( - // 不区分大小写。 - new[] { expectedFilePath }, - new[] { "Print", expectedFilePath }); - } - - [ContractTestCase] - public void HandleAsync() - { - const string expectedFilePath = @"C:\Users\lvyi\Test.txt"; - - "处理带有谓词的命令行参数,可以正确根据谓词选择处理函数。".Test(async (string[] args, int expectedExitCode) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - var exitCode = await commandLine - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - return 1; - }) - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - return 2; - }) - .RunAsync().ConfigureAwait(false); - - // Assert - Assert.AreEqual(expectedExitCode, exitCode); - }).WithArguments( - // 不区分大小写。 - (new[] { "Edit", expectedFilePath }, 1), - (new[] { "edit", expectedFilePath }, 1), - (new[] { "Print", expectedFilePath }, 2)); - - "处理带有谓词的命令行参数,可以正确解析出含谓词的命令行参数。".Test(async (string[] args) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - string? filePath = null; - await commandLine - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - filePath = options.FilePath; - }) - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - filePath = options.FilePath; - }) - .AddHandler( -#pragma warning disable 1998 - async options => { } -#pragma warning restore 1998 - ) - .RunAsync().ConfigureAwait(false); - - // Assert - Assert.AreEqual(expectedFilePath, filePath); - }).WithArguments( - // 不区分大小写。 - new[] { "Edit", expectedFilePath }, - new[] { "Print", expectedFilePath }); - - "处理带有默认谓词的命令行参数,可以在没有谓词的情况下解析。".Test(async (string[] args) => - { - // Arrange - var commandLine = CommandLine.Parse(args); - - // Action - string? filePath = null; - await commandLine - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - filePath = options.FilePath; - }) - .AddHandler(async options => - { - await Task.Delay(10).ConfigureAwait(false); - filePath = options.FilePath; - }) - .RunAsync().ConfigureAwait(false); - - // Assert - Assert.AreEqual(expectedFilePath, filePath); - }).WithArguments( - // 不区分大小写。 - new[] { expectedFilePath }, - new[] { "Print", expectedFilePath }); - } - } -} diff --git a/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj b/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj index a41ba65d..4f45e371 100644 --- a/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj +++ b/tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj @@ -18,14 +18,6 @@ - - - - - - - - ..\dotnetCampus.CommandLine.Performance\dotnetCampus.CommandLine.Legacy.dll From 71f8ede05a5ef875b1b092df5619694a627dfb20 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 13:08:01 +0800 Subject: [PATCH 113/193] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => ParsingOptions}/CommandLineStyleMagicNumberTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/DotNetCampus.CommandLine.Tests/{ => ParsingOptions}/CommandLineStyleMagicNumberTests.cs (86%) diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingOptions/CommandLineStyleMagicNumberTests.cs similarity index 86% rename from tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs rename to tests/DotNetCampus.CommandLine.Tests/ParsingOptions/CommandLineStyleMagicNumberTests.cs index bd1693f8..a307a4e5 100644 --- a/tests/DotNetCampus.CommandLine.Tests/CommandLineStyleMagicNumberTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingOptions/CommandLineStyleMagicNumberTests.cs @@ -1,6 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace DotNetCampus.Cli.Tests; +namespace DotNetCampus.Cli.Tests.ParsingOptions; [TestClass] public class CommandLineStyleMagicNumberTests From e3e0cfeba0667a2ef2a132760687fff81c30bf7f Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 14:00:02 +0800 Subject: [PATCH 114/193] =?UTF-8?q?=E6=9B=B4=E6=94=B9=20Throws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandMatching/MatchCommandTests.cs | 2 +- .../ParsingStyles/OptionBooleanValueTests.cs | 8 ++++---- .../ParsingStyles/OptionCaseSensitiveTests.cs | 2 +- .../ParsingStyles/OptionCollectionValueTests.cs | 4 ++-- .../ParsingStyles/OptionNamingPolicyTests.cs | 2 +- .../ParsingStyles/OptionValueSeparatorTests.cs | 10 +++++----- .../ParsingStyles/PositionalArgumentTests.cs | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs index 914624f7..56b40e65 100644 --- a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/MatchCommandTests.cs @@ -77,7 +77,7 @@ public void MatchCommand_PositionalArgumentNotMatch(string[] args, TestCommandLi var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine + var exception = Assert.ThrowsExactly(() => commandLine .AddHandler(o => matched = o.Value) .AddHandler(o => matched = o.Value) .AddHandler(o => matched = o.Value) diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs index f5f70fc2..2304e297 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionBooleanValueTests.cs @@ -115,7 +115,7 @@ public void GnuDoesNotSupportExplicitBooleanValue(string[] args, TestCommandLine var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); @@ -145,7 +145,7 @@ public void OptionCombinationMustAllBoolean(string[] args, TestCommandLineStyle var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.ArgumentCombinationIsNotBoolean, exception.Reason); @@ -163,7 +163,7 @@ public void DoesNotSupportBooleanOptionCombination(string[] args, TestCommandLin var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); @@ -200,7 +200,7 @@ public void MultiCharShortOptionsDoesNotSupportValue(string[] args, TestCommandL var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs index eeaa4cc3..9a47bd92 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCaseSensitiveTests.cs @@ -19,7 +19,7 @@ public void CaseSensitive(string[] args, TestCommandLineStyle style) var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs index 8a6951b2..c0652a84 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionCollectionValueTests.cs @@ -115,7 +115,7 @@ public void NotSupported_Collection(string[] args, TestCommandLineStyle style) var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); @@ -149,7 +149,7 @@ public void DoesNotSupportOptionWithValueAndArgumentValueCollection(string[] arg var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs index 9131f975..3a9afd5f 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionNamingPolicyTests.cs @@ -58,7 +58,7 @@ public void NotSupported(string[] args, TestCommandLineStyle style) var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs index 61d66fef..d9dbf421 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/OptionValueSeparatorTests.cs @@ -90,7 +90,7 @@ public void NotSupported(string[] args, TestCommandLineStyle style) var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentSeparatorNotSupported, exception.Reason); @@ -123,7 +123,7 @@ public void DoesNotSupportShortOptionWithoutSeparator(string[] args, TestCommand var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); @@ -137,7 +137,7 @@ public void UrlStyleDoesNotSupportShortOption(string[] args, TestCommandLineStyl var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); @@ -181,7 +181,7 @@ public void DoesNotSupportMultiCharShortOptions(string[] args, TestCommandLineSt var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); @@ -196,7 +196,7 @@ public void DoesNotSupportMultiCharShortOptionsWithValue(string[] args, TestComm var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.MultiCharShortOptionalArgumentNotSupported, exception.Reason); diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs index efde0cbd..0f640f89 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/PositionalArgumentTests.cs @@ -104,7 +104,7 @@ public void DoesNotSupportPostPositionalArguments(string[] args, TestCommandLine var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.OptionalArgumentNotFound, exception.Reason); @@ -118,7 +118,7 @@ public void DoesNotMatchPositionalArgumentRange_Boolean(string[] args, TestComma var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); @@ -134,7 +134,7 @@ public void DoesNotMatchPositionalArgumentRange_Collection(string[] args, TestCo var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var exception = Assert.Throws(() => commandLine.As()); + var exception = Assert.ThrowsExactly(() => commandLine.As()); // Assert Assert.AreEqual(CommandLineParsingError.PositionalArgumentNotFound, exception.Reason); From f98d3340c26139f8271079667a46c8eef69128d1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 14:00:11 +0800 Subject: [PATCH 115/193] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/DefaultValueTests.cs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs new file mode 100644 index 00000000..e6b55a21 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs @@ -0,0 +1,302 @@ +using System.Collections; +using System.Collections.Generic; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Exceptions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotNetCampus.Cli.Tests.ParsingStyles; + +[TestClass] +public class DefaultValueTests +{ + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void Required_ThrowsException(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act & Assert + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + Assert.ThrowsExactly(() => commandLine.As()); + } + + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void WithoutInit_KeepsDefaultValue(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var options = commandLine.As(); + var optionsWithNullable = commandLine.As(); + var optionsWithCollection = commandLine.As(); + var optionsWithNullableCollection = commandLine.As(); + + // Assert + Assert.AreEqual("Default", options.Option); + Assert.AreEqual("Default", optionsWithNullable.Option); + CollectionAssert.AreEqual(new[] { "Default" }, (ICollection)optionsWithCollection.Option); + CollectionAssert.AreEqual(new[] { "Default" }, (ICollection)optionsWithNullableCollection.Option!); + } + + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void InitCollection_AlwaysNotNullEmpty(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var optionsWithInitCollection = commandLine.As(); + var optionsWithNullableInitCollection = commandLine.As(); + + // Assert + Assert.IsNotNull(optionsWithInitCollection.Option); + Assert.IsNotNull(optionsWithNullableInitCollection.Option); + Assert.IsEmpty(optionsWithInitCollection.Option); + Assert.IsEmpty(optionsWithNullableInitCollection.Option); + } + + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void NullableInitStruct_Null(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var optionsWithInitNullableStruct = commandLine.As(); + + // Assert + Assert.IsNull(optionsWithInitNullableStruct.Option); + } + + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void InitStruct_Default(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var optionsWithInitStruct = commandLine.As(); + + // Assert + Assert.AreEqual(0, optionsWithInitStruct.Option); + } + + public record OptionsWithRequired + { + [Option('o', "option")] + public required string Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithRequiredInit + { + [Option('o', "option")] + public required string Option { get; init; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithRequiredCollection + { + [Option('o', "option")] + public required IReadOnlyList Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithRequiredInitCollection + { + [Option('o', "option")] + public required IReadOnlyList Option { get; init; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableRequired + { + [Option('o', "option")] + public required string? Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableRequiredInit + { + [Option('o', "option")] + public required string? Option { get; init; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableRequiredCollection + { + [Option('o', "option")] + public required IReadOnlyList? Option { get; set; } + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableRequiredInitCollection + { + [Option('o', "option")] + public required IReadOnlyList? Option { get; init; } + + [Value(0)] + public string? Value { get; set; } + } + + public record Options + { + [Option('o', "option")] + public string Option { get; set; } = "Default"; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithInit + { + [Option('o', "option")] + public string Option { get; init; } = "Default"; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithCollection + { + [Option('o', "option")] + public IReadOnlyList Option { get; set; } = ["Default"]; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithInitCollection + { + [Option('o', "option")] + public IReadOnlyList Option { get; init; } = ["Default"]; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullable + { + [Option('o', "option")] + public string? Option { get; set; } = "Default"; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableInit + { + [Option('o', "option")] + public string? Option { get; init; } = "Default"; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableCollection + { + [Option('o', "option")] + public IReadOnlyList? Option { get; set; } = ["Default"]; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithNullableInitCollection + { + [Option('o', "option")] + public IReadOnlyList? Option { get; init; } = ["Default"]; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithInitNullableStruct + { + [Option('o', "option")] + public int? Option { get; init; } = 42; + + [Value(0)] + public string? Value { get; set; } + } + + public record OptionsWithInitStruct + { + [Option('o', "option")] + public int Option { get; init; } = 42; + + [Value(0)] + public string? Value { get; set; } + } +} From 656d5f0bec1010505af439b4094324477333a9e1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 14:59:32 +0800 Subject: [PATCH 116/193] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=A7=84=E5=AE=9A?= =?UTF-8?q?=E5=B9=B6=E6=B5=8B=E8=AF=95=E5=B1=9E=E6=80=A7=E5=88=9D=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/README.md | 18 +++--- docs/zh-hans/README.md | 27 ++++---- docs/zh-hant/README.md | 20 +++--- .../Generators/ModelBuilderGenerator.cs | 63 ++++++++---------- .../Compiler/PropertyAssignments.cs | 64 +++++++++---------- .../ParsingStyles/DefaultValueTests.cs | 49 ++++++++++---- 6 files changed, 129 insertions(+), 112 deletions(-) diff --git a/docs/en/README.md b/docs/en/README.md index 345d9e43..ad7ef2b7 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -210,17 +210,19 @@ When defining a property, these modifiers apply: What value a property ultimately receives depends on: -| required | init | Collection | nullable | Behavior | Explanation | -| -------- | ---- | ---------- | -------- | ---------------- | ----------------------------------------------- | -| 1 | _ | _ | _ | Throw | Must be supplied; missing raises exception | -| 0 | 1 | 1 | _ | Empty collection | Collections are never null; missing => empty | -| 0 | 1 | 0 | 1 | null | Nullable; missing => null | -| 0 | 1 | 0 | 0 | Default value | Non-nullable; missing => default(T) | -| 0 | 0 | _ | _ | Keep initial | Not required/immediate; keeps initializer value | +| required | init | nullable | Collection | Behavior | Explanation | +| -------- | ---- | -------- | ---------- | ------------------- | -------------------------------------------------- | +| 1 | _ | _ | _ | Throw | Must be supplied; missing throws exception | +| 0 | 1 | 1 | _ | null | Nullable; missing => null | +| 0 | 1 | 0 | 1 | Empty collection | Collections are never null; missing => empty | +| 0 | 1 | 0 | 0 | Default/empty value | Non-nullable; missing => default value[^2] | +| 0 | 0 | _ | _ | Keep initial | Not required or immediate; keeps initializer value | + +[^2]: If it's a value type, it receives its default value; if it's a reference type (currently only string), it becomes the empty string `""`. - 1 = present - 0 = absent -- _ = regardless +- _ = regardless of presence 1. Nullable behavior is the same for reference and value types (default value just yields `null` for reference types) 2. Missing required option throws `RequiredPropertyNotAssignedException` diff --git a/docs/zh-hans/README.md b/docs/zh-hans/README.md index 4b73b518..3fe6fb15 100644 --- a/docs/zh-hans/README.md +++ b/docs/zh-hans/README.md @@ -202,21 +202,24 @@ public class Options ## 必需选项与默认值 -当你定义一个属性的时候,有这些标记可用: +当你定义一个属性的时候,这些标记会影响到默认值: -1. 使用 `required` 标记一个选项是必须的 -1. 使用 `init` 标记一个选项是不可变的 -1. 使用 `?` 标记一个选项是可空的 +1. `required`:标记一个属性是必须的 +1. `init`:标记一个属性是不可变的 +1. `?`:标记一个属性是可空的 +1. 特别的,集合类型也会有特别处理 -而具体会被赋成什么值取决于以下这些因素: +这些行为具体以如下表格影响着属性的初值: -| required | init | 集合属性 | nullable | 行为 | 解释 | -| -------- | ---- | -------- | -------- | -------- | ----------------------------------- | -| 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | -| 0 | 1 | 1 | _ | 空集合 | 集合永不为 `null`,没传就赋值空集合 | -| 0 | 1 | 0 | 1 | `null` | 可空,没有传就赋值 `null` | -| 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | -| 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | +| required | init | nullable | list | 行为 | 解释 | +| -------- | ---- | -------- | ---- | ----------- | --------------------------------- | +| 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | +| 0 | 1 | 1 | _ | null | 可空,没有传就赋值 null | +| 0 | 1 | 0 | 1 | 空集合 | 集合永不为 null,没传就赋值空集合 | +| 0 | 1 | 0 | 0 | 默认值/空值 | 不可空,没有传就赋值默认值[^2] | +| 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | + +[^2]: 如果是值类型,则会赋值其默认值;如果是引用类型,目前只有一种情况,就是字符串,会赋值为空字符串 `""`。 - 1 = 标记了 - 0 = 没标记 diff --git a/docs/zh-hant/README.md b/docs/zh-hant/README.md index 2c6fb885..bf8989f4 100644 --- a/docs/zh-hant/README.md +++ b/docs/zh-hant/README.md @@ -210,19 +210,21 @@ public class Options 實際指派的值依下表行為: -| required | init | 集合屬性 | nullable | 行為 | 說明 | -| -------- | ---- | -------- | -------- | ---------- | ------------------------------- | -| 1 | _ | _ | _ | 擲出例外 | 必須傳入,缺少則擲出例外 | -| 0 | 1 | 1 | _ | 空集合 | 集合永不為 null,缺少則給空集合 | -| 0 | 1 | 0 | 1 | null | 可為 null,缺少則給 null | -| 0 | 1 | 0 | 0 | 預設值 | 不可為 null,缺少則 default(T) | -| 0 | 0 | _ | _ | 保留初始值 | 非必需/非立即,保留定義時初始值 | +| required | init | nullable | 集合屬性 | 行為 | 說明 | +| -------- | ---- | -------- | -------- | -------------- | --------------------------------- | +| 1 | _ | _ | _ | 擲出例外 | 必須傳入,缺少則擲出例外 | +| 0 | 1 | 1 | _ | null | 可為 null,缺少則給 null | +| 0 | 1 | 0 | 1 | 空集合 | 集合永不為 null,缺少則給空集合 | +| 0 | 1 | 0 | 0 | 預設值/空值 | 不可為 null,缺少則給預設值[^2] | +| 0 | 0 | _ | _ | 保留初始值 | 非必需或非立即,保留定義時初始值 | + +[^2]: 如果是值型別,則會賦值其預設值;如果是參考型別,目前只有一種情況,就是字串,會賦值為空字串 `""`。 - 1 = 標記過 - 0 = 未標記 -- _ = 不論 +- _ = 不論是否標記 -1. 可空行為對參考與值型別一致(差別只是 default 對參考型別為 null) +1. 可空行為對參考與值型別一致(差別只是預設值對參考型別為 null) 2. 缺少必需選項會擲出 `RequiredPropertyNotAssignedException` 3. 「保留初始值」表示可直接在屬性定義時給初值: diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index ff0d88b6..58482ed3 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -418,17 +418,22 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau // list: 属性是一个集合 // cli: 实际命令行参数是否传入 - // | required | init | list | nullable | 行为 | 解释 | - // | -------- | ---- | ---- | -------- | ---------- | --------------------------------- | - // | 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | - // | 0 | 1 | 1 | _ | 空集合 | 集合永不为 null,没传就赋值空集合 | - // | 0 | 1 | 0 | 1 | null | 可空,没有传就赋值 null | - // | 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | - // | 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | + // | required | init | nullable | list | 行为 | 解释 | + // | -------- | ---- | -------- | ---- | ----------- | --------------------------------- | + // | 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | + // | 0 | 1 | 1 | _ | null | 可空,没有传就赋值 null | + // | 0 | 1 | 0 | 1 | 空集合 | 集合永不为 null,没传就赋值空集合 | + // | 0 | 1 | 0 | 0 | 默认值/空值 | 不可空,没有传就赋值默认值 | + // | 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | + // + // [默认值/空值] 如果是值类型,则会赋值其默认值;如果是引用类型,目前只有一种情况,就是字符串,会赋值为空字符串 `""`。 var toTarget = model.Type.GetGeneratedNotAbstractTypeName(); - var isList = model.Type.AsCommandValueKind() is CommandValueKind.List or CommandValueKind.Dictionary; - var fallback = (model.IsRequired, model.IsInitOnly, isList, model.IsNullable) switch + var kind = model.Type.AsCommandValueKind(); + var isString = kind is CommandValueKind.String; + var isList = kind is CommandValueKind.List or CommandValueKind.Dictionary; + var supportCollectionExpression = model.Type.SupportCollectionExpression(false); + var fallback = (model.IsRequired, model.IsInitOnly, model.IsNullable, isList) switch { (true, _, _, _) => model switch { @@ -438,39 +443,21 @@ private string GenerateInitProperty(PropertyGeneratingModel model, bool forDefau $"throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($\"The command line arguments doesn't contain a required positional argument at index {positionalArgument.Index}. Command line: {{commandLine}}\", \"{positionalArgument.PropertyName}\")", _ => "", }, - (_, true, true, _) => "", - (_, true, false, true) => "null", - (_, true, false, false) => $"default({model.Type.ToDisplayString()})!", + (_, true, true, _) => "null", + (_, true, false, true) => supportCollectionExpression + ? "[]" + : $"new {GetArgumentPropertyTypeName(model)}().To{toTarget}(true)", + (_, true, false, false) => isString + ? "\"\"" + : $"default({model.Type.ToDisplayString()})!", _ => "/* 非 init 属性,在下面单独赋值 */", }; - if (!forDefault) - { + return !forDefault // 正常传入了命令行参数时的通用赋值。 - return $"{model.PropertyName} = {model.PropertyName}.To{toTarget}(){(fallback is "" ? "" : $" ?? {fallback}")},"; - } - - if (fallback is not "") - { - // 未传命令行参数时,给非集合类型赋值。 - return $"{model.PropertyName} = {fallback},"; - } - - // 未传命令行参数时,给集合类型赋值为空集合。 - var supportCollectionExpression = model.Type.SupportCollectionExpression(true); - var supportCollectionExpressionLegacy = model.Type.SupportCollectionExpression(false); - return (supportCollectionExpression, supportCollectionExpressionLegacy) switch - { - (true, true) => $" {model.PropertyName} = [],", - (false, false) => $" {model.PropertyName} = new {GetArgumentPropertyTypeName(model)}().To{toTarget}(),", - _ => $""" - #if NET8_0_OR_GREATER - {model.PropertyName} = [], - #else - {model.PropertyName} = new {GetArgumentPropertyTypeName(model)}().To{toTarget}(), - #endif - """, - }; + ? $"{model.PropertyName} = {model.PropertyName}.To{toTarget}(){(fallback is "" ? "" : $" ?? {fallback}")}," + // 未传命令行参数时,直接赋回退值。 + : $"{model.PropertyName} = {fallback},"; } private string GenerateSetProperty(PropertyGeneratingModel model, int modelIndex) diff --git a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs index d5d921a1..49c8e341 100644 --- a/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs +++ b/src/DotNetCampus.CommandLine/Compiler/PropertyAssignments.cs @@ -221,54 +221,54 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 将解析到的值转换为集合。 /// - public Collection ToCollection() => Value switch + public Collection? ToCollection() => Value switch { - null or { Count: 0 } => [], + null or { Count: 0 } => null, { } values => [..values], }; /// /// 将解析到的值转换为字符串数组。 /// - public string[] ToArray() => Value switch + public string[]? ToArray() => Value switch { - null or { Count: 0 } => [], + null or { Count: 0 } => null, { } values => [..values], }; /// /// 将解析到的值转换为哈希集合。 /// - public HashSet ToHashSet() => Value switch + public HashSet? ToHashSet() => Value switch { - null or { Count: 0 } => [], + null or { Count: 0 } => null, { } values => [..values], }; /// /// 将解析到的值转换为列表。 /// - public List ToList() => Value switch + public List? ToList() => Value switch { - null or { Count: 0 } => [], + null or { Count: 0 } => null, { } values => values, }; /// /// 将解析到的值转换为只读集合。 /// - public ReadOnlyCollection ToReadOnlyCollection() => Value switch + public ReadOnlyCollection? ToReadOnlyCollection() => Value switch { - null or { Count: 0 } => new ReadOnlyCollection([]), + null or { Count: 0 } => null, { } values => new ReadOnlyCollection(values), }; /// /// 将解析到的值转换为排序集合。 /// - public SortedSet ToSortedSet() => Value switch + public SortedSet? ToSortedSet() => Value switch { - null or { Count: 0 } => [], + null or { Count: 0 } => null, { } values => [..values], }; @@ -276,13 +276,13 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变数组。 /// - public ImmutableArray ToImmutableArray() => Value switch + public ImmutableArray? ToImmutableArray(bool notNull = false) => Value switch { #if NET8_0_OR_GREATER - null or { Count: 0 } => [], + null or { Count: 0 } => notNull ? ImmutableArray.Empty : null, { } values => [..values], #else - null or { Count: 0 } => ImmutableArray.Empty, + null or { Count: 0 } => null, { } values => values.ToImmutableArray(), #endif }; @@ -290,13 +290,13 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变列表。 /// - public ImmutableList ToImmutableList() => Value switch + public ImmutableList? ToImmutableList(bool notNull = false) => Value switch { #if NET8_0_OR_GREATER - null or { Count: 0 } => [], + null or { Count: 0 } => notNull ? ImmutableList.Empty : null, { } values => [..values], #else - null or { Count: 0 } => ImmutableList.Empty, + null or { Count: 0 } => null, { } values => values.ToImmutableList(), #endif }; @@ -304,13 +304,13 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变排序集合。 /// - public ImmutableSortedSet ToImmutableSortedSet() => Value switch + public ImmutableSortedSet? ToImmutableSortedSet(bool notNull = false) => Value switch { #if NET8_0_OR_GREATER - null or { Count: 0 } => [], + null or { Count: 0 } => notNull ? ImmutableSortedSet.Empty : null, { } values => [..values], #else - null or { Count: 0 } => ImmutableSortedSet.Empty, + null or { Count: 0 } => null, { } values => values.ToImmutableSortedSet(), #endif }; @@ -318,13 +318,13 @@ public StringListArgument Append(ReadOnlySpan value) /// /// 将解析到的值转换为不可变哈希集合。 /// - public ImmutableHashSet ToImmutableHashSet() => Value switch + public ImmutableHashSet? ToImmutableHashSet(bool notNull = false) => Value switch { #if NET8_0_OR_GREATER - null or { Count: 0 } => [], + null or { Count: 0 } => notNull ? ImmutableHashSet.Empty : null, { } values => [..values], #else - null or { Count: 0 } => ImmutableHashSet.Empty, + null or { Count: 0 } => null, { } values => values.ToImmutableHashSet(), #endif }; @@ -379,17 +379,17 @@ public StringDictionaryArgument Append(ReadOnlySpan key, ReadOnlySpan /// 将解析到的值转换为字典。 ///
- public Dictionary ToDictionary() + public Dictionary? ToDictionary() { - return Value ?? []; + return Value; } /// /// 将解析到的值转换为排序字典。 /// - public SortedDictionary ToSortedDictionary() => Value switch + public SortedDictionary? ToSortedDictionary() => Value switch { - null or { Count: 0 } => new SortedDictionary(), + null or { Count: 0 } => null, { } values => new SortedDictionary(values), }; @@ -397,18 +397,18 @@ public Dictionary ToDictionary() /// /// 将解析到的值转换为不可变字典。 /// - public ImmutableDictionary ToImmutableDictionary() => Value switch + public ImmutableDictionary? ToImmutableDictionary(bool notNull = false) => Value switch { - null or { Count: 0 } => ImmutableDictionary.Empty, + null or { Count: 0 } => notNull ? ImmutableDictionary.Empty : null, { } values => values.ToImmutableDictionary(), }; /// /// 将解析到的值转换为不可变排序字典。 /// - public ImmutableSortedDictionary ToImmutableSortedDictionary() => Value switch + public ImmutableSortedDictionary? ToImmutableSortedDictionary(bool notNull = false) => Value switch { - null or { Count: 0 } => ImmutableSortedDictionary.Empty, + null or { Count: 0 } => notNull ? ImmutableSortedDictionary.Empty : null, { } values => values.ToImmutableSortedDictionary(), }; #endif diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs index e6b55a21..431e7b1b 100644 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/DefaultValueTests.cs @@ -76,20 +76,20 @@ public void WithoutInit_KeepsDefaultValue(string[] args, TestCommandLineStyle st [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] - public void InitCollection_AlwaysNotNullEmpty(string[] args, TestCommandLineStyle style) + public void InitNullable_AssignsNull(string[] args, TestCommandLineStyle style) { // Arrange var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var optionsWithInitCollection = commandLine.As(); + var optionsWithNullableInit = commandLine.As(); var optionsWithNullableInitCollection = commandLine.As(); + var optionsWithInitNullableValueType = commandLine.As(); // Assert - Assert.IsNotNull(optionsWithInitCollection.Option); - Assert.IsNotNull(optionsWithNullableInitCollection.Option); - Assert.IsEmpty(optionsWithInitCollection.Option); - Assert.IsEmpty(optionsWithNullableInitCollection.Option); + Assert.IsNull(optionsWithNullableInit.Option); + Assert.IsNull(optionsWithNullableInitCollection.Option); + Assert.IsNull(optionsWithInitNullableValueType.Option); } [TestMethod] @@ -103,16 +103,16 @@ public void InitCollection_AlwaysNotNullEmpty(string[] args, TestCommandLineStyl [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] - public void NullableInitStruct_Null(string[] args, TestCommandLineStyle style) + public void InitCollection_Empty(string[] args, TestCommandLineStyle style) { // Arrange var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var optionsWithInitNullableStruct = commandLine.As(); + var optionsWithCollection = commandLine.As(); // Assert - Assert.IsNull(optionsWithInitNullableStruct.Option); + Assert.IsEmpty(optionsWithCollection.Option); } [TestMethod] @@ -126,13 +126,36 @@ public void NullableInitStruct_Null(string[] args, TestCommandLineStyle style) [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] - public void InitStruct_Default(string[] args, TestCommandLineStyle style) + public void InitString_Empty(string[] args, TestCommandLineStyle style) { // Arrange var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); // Act - var optionsWithInitStruct = commandLine.As(); + var optionsWithInit = commandLine.As(); + + // Assert + Assert.IsEmpty(optionsWithInit.Option); + } + + [TestMethod] + [DataRow(new string[] { }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible]")] + [DataRow(new string[] { }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet]")] + [DataRow(new string[] { }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu]")] + [DataRow(new string[] { }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell]")] + [DataRow(new[] { "test://" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Flexible, DisplayName = "[Flexible] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.DotNet, DisplayName = "[DotNet] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.Gnu, DisplayName = "[Gnu] value")] + [DataRow(new[] { "value" }, TestCommandLineStyle.PowerShell, DisplayName = "[PowerShell] value")] + [DataRow(new[] { "test://value" }, TestCommandLineStyle.Url, DisplayName = "[Url] test://value")] + public void InitValueType_Default(string[] args, TestCommandLineStyle style) + { + // Arrange + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var optionsWithInitStruct = commandLine.As(); // Assert Assert.AreEqual(0, optionsWithInitStruct.Option); @@ -282,7 +305,7 @@ public record OptionsWithNullableInitCollection public string? Value { get; set; } } - public record OptionsWithInitNullableStruct + public record OptionsWithInitNullableValueType { [Option('o', "option")] public int? Option { get; init; } = 42; @@ -291,7 +314,7 @@ public record OptionsWithInitNullableStruct public string? Value { get; set; } } - public record OptionsWithInitStruct + public record OptionsWithInitValueType { [Option('o', "option")] public int Option { get; init; } = 42; From 96aa699c0e3783b2a08665c2fcd2b251f9dbad27 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 15:03:54 +0800 Subject: [PATCH 117/193] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=BB=99=20AI=20?= =?UTF-8?q?=E7=9C=8B=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParsingStyles/README.md | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md diff --git a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md b/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md deleted file mode 100644 index 1bca9acb..00000000 --- a/tests/DotNetCampus.CommandLine.Tests/ParsingStyles/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# 规则一览 - -## 命令行风格 - -```csharp -// 使用 .NET CLI 风格解析命令行参数 -var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); -``` - -支持的风格包括: - -- `CommandLineStyle.Flexible`(默认):灵活风格,在各种风格间提供最大的兼容性,默认大小写不敏感 -- `CommandLineStyle.DotNet`:.NET CLI 风格,默认大小写敏感 -- `CommandLineStyle.Gnu`:符合 GNU 规范的风格,默认大小写敏感 -- `CommandLineStyle.Posix`:符合 POSIX 规范的风格,默认大小写敏感 -- `CommandLineStyle.PowerShell`:PowerShell 风格,默认大小写不敏感 - -默认情况下,这些风格的详细区别如下: - -| 风格 | Flexible | DotNet | Gnu | Posix | PowerShell | URL | -| ----------------- | -------------- | -------------- | ----------------- | ---------- | ------------ | ----------------- | -| 大小写 | 不敏感 | 敏感 | 敏感 | 敏感 | 不敏感 | 不敏感 | -| 长选项 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | -| 短选项 | 支持 | 支持 | 支持 | 支持 | 支持 | 不支持 | -| 长选项前缀 | `--` `-` `/` | `--` | `--` | 不支持 | `-` `/` | | -| 短选项前缀 | `-` `/` | `-` | `-` | `-` | `-` `/` | | -| 长选项 ` ` | --option value | --option value | -o value | -o value | -o value | | -| 长选项 `=` | --option=value | --option=value | --option=value | | -o=value | option=value | -| 长选项 `:` | --option:value | --option:value | | | -o:value | | -| 短选项 ` ` | -o value | -o value | -o value | -o value | -o value | | -| 短选项 `=` | -o=value | -o=value | | | -o=value | option=value | -| 短选项 `:` | -o:value | -o:value | | | -o:value | | -| 短选项 `null` | | | -ovalue | | | | -| 多字符短选项 | -abc value | -abc value | | | -abc value | | -| 长布尔选项 | --option | --option | --option | | -Option | option | -| 长布尔选项 ` ` | --option true | --option true | | | -Option true | | -| 长布尔选项 `=` | --option=true | --option=true | --option=true[^1] | | -Option=true | | -| 长布尔选项 `:` | --option:true | --option:true | | | -Option:true | | -| 短选项选项 | -o | -o | -o | -o | -o | | -| 短选项选项 ` ` | -o true | -o true | | | -o true | | -| 短选项选项 `=` | -o=true | -o=true | | | -o=true | option=true | -| 短选项选项 `:` | -o:true | -o:true | | | -o:true | | -| 短选项选项 `null` | | | -o1 | | | | -| 布尔/开关值 | true/false | true/false | true/false | true/false | true/false | true/false | -| 布尔/开关值 | yes/no | yes/no | yes/no | yes/no | yes/no | yes/no | -| 布尔/开关值 | on/off | on/off | on/off | on/off | on/off | on/off | -| 布尔/开关值 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | 1/0 | -| 短布尔选项合并 | | | -abc | -abc | | | -| 集合选项 | -o A -o B | -o A -o B | -o A -o B | -o A -o B | -o A -o B | option=A&option=B | -| 集合选项 ` ` | -o A B C | -o A B C | | | -o A B C | | -| 集合选项 `,` | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | -o A,B,C | | -| 集合选项 `;` | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | -o A;B;C | | -| 字典选项 | -o:A=X;B=Y | -o:A=X;B=Y | | | -o:A=X;B=Y | | -| 命名法 | --kebab-case | --kebab-case | --kebab-case | | | kebab-case | -| 命名法 | -PascalCase | | | | -PascalCase | | -| 命名法 | -camelCase | | | | -camelCase | | -| 命名法 | /PascalCase | | | | /PascalCase | | -| 命名法 | /camelCase | | | | /camelCase | | -| 位置参数 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | -| 后置位置参数 `--` | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | - -[^1]: GNU 风格并不支持布尔选项显式带值,但因为这种情况并没有歧义,所以我们考虑额外支持它。 - -## 必需选项与默认值 - -当你定义一个属性的时候,有这些标记可用: - -1. 使用 `required` 标记一个选项是必须的 -1. 使用 `init` 标记一个选项是不可变的 -1. 使用 `?` 标记一个选项是可空的 - -而具体会被赋成什么值取决于以下这些因素: - -| required | init | 集合属性 | nullable | 行为 | 解释 | -| -------- | ---- | -------- | -------- | -------- | ----------------------------------- | -| 1 | _ | _ | _ | 抛异常 | 要求必须传入,没有传就抛异常 | -| 0 | 1 | 1 | _ | 空集合 | 集合永不为 `null`,没传就赋值空集合 | -| 0 | 1 | 0 | 1 | `null` | 可空,没有传就赋值 `null` | -| 0 | 1 | 0 | 0 | 默认值 | 不可空,没有传就赋值默认值 | -| 0 | 0 | _ | _ | 保留初值 | 不要求必须或立即赋值的,保留初值 | - -- 1 = 标记了 -- 0 = 没标记 -- _ = 无论有没有标记 - -1. 可空,无论是引用类型还是值类型,其行为完全一致。要硬说不同,就是那个「默认值」会导致引用类型得到 `null`。 -2. 如果未提供必需选项,解析时会抛出`RequiredPropertyNotAssignedException`异常。 -3. 上述行为的「保留初值」的意思是,你可以在定义这个属性的时候写一个初值,就像下面这样: - -```csharp -// 请注意,这里的初值仅在没有 required 也没有 init 时才生效。 -[Option('o', "option-name")] -public string OptionName { get; set; } = "Default Value" -``` From 13fbc7c4cb310d331c9a392647a572f9fd93dd5c Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 21 Sep 2025 16:41:24 +0800 Subject: [PATCH 118/193] =?UTF-8?q?=E6=9F=A5=E6=89=BE=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E5=B9=B6=E6=8A=A5=E5=91=8A=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnalyzerReleases.Shipped.md | 1 + .../Diagnostics.cs | 23 +++-- .../Generators/ModelBuilderGenerator.cs | 35 ++++++-- ...OptionalArgumentPropertyGeneratingModel.cs | 83 +++++++++++++++++++ .../Models/PropertyGeneratingModel.cs | 3 + .../Properties/Localizations.Designer.cs | 27 ++++++ .../Properties/Localizations.resx | 9 ++ .../Properties/Localizations.zh-hans.resx | 9 ++ .../DotNetCampus.CommandLine.Tests.csproj | 6 -- .../Issues/OptionNameConflictionTests.cs | 21 +++++ 10 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 tests/DotNetCampus.CommandLine.Tests/Issues/OptionNameConflictionTests.cs diff --git a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md index dc0b2513..7d53d48c 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md +++ b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md @@ -11,6 +11,7 @@ DCL102 | DotNetCampus.AvoidBugs | Info |