├── .gitattributes ├── .github └── workflows │ ├── dotnet-build.yml │ └── nuget-tag-publish.yml ├── .gitignore ├── ApiChanges.3.x-4.0.md ├── Directory.Build.props ├── DotNetCampus.CommandLine.sln ├── LICENSE ├── README.md ├── build └── Version.props ├── docs ├── analyzers │ └── DCL101.md ├── en │ └── README.md ├── zh-hans │ └── README.md └── zh-hant │ └── README.md ├── samples └── DotNetCampus.CommandLine.Sample │ ├── DefaultOptions.cs │ ├── DotNetCampus.CommandLine.Sample.csproj │ ├── Program.cs │ ├── Properties │ ├── LocalizableStrings.Designer.cs │ ├── LocalizableStrings.resx │ ├── LocalizableStrings.zh-CN.resx │ ├── LocalizableStrings.zh-CN.zh-hans-cn.resx │ ├── LocalizableStrings.zh-hans-cn.resx │ └── launchSettings.json │ └── SampleOptions.cs ├── src ├── DotNetCampus.CommandLine.Analyzer │ ├── AnalyzerReleases.Shipped.md │ ├── AnalyzerReleases.Unshipped.md │ ├── Analyzers │ │ ├── ConvertOptionProperty │ │ │ ├── ConvertOptionPropertyTypeCodeFix.cs │ │ │ ├── FindOptionPropertyTypeAnalyzer.cs │ │ │ ├── OptionPropertyTypeToBooleanCodeFix.cs │ │ │ ├── OptionPropertyTypeToDictionaryCodeFix.cs │ │ │ ├── OptionPropertyTypeToDoubleCodeFix.cs │ │ │ ├── OptionPropertyTypeToInt32CodeFix.cs │ │ │ ├── OptionPropertyTypeToListCodeFix.cs │ │ │ └── OptionPropertyTypeToStringCodeFix.cs │ │ ├── OptionLongNameMustBeKebabCaseAnalyzer.cs │ │ └── OptionLongNameMustBeKebabCaseCodeFixProvider.cs │ ├── Diagnostics.cs │ ├── DotNetCampus.CommandLine.Analyzer.csproj │ ├── GeneratorInfo.cs │ ├── Generators │ │ ├── BuilderGenerator.cs │ │ ├── InterceptorGenerator.cs │ │ └── ModelProviding │ │ │ ├── CommandModelProvider.cs │ │ │ └── InterceptorModelProvider.cs │ ├── Properties │ │ ├── Localizations.Designer.cs │ │ ├── Localizations.resx │ │ ├── Localizations.zh-hans-cn.resx │ │ └── launchSettings.json │ └── Utils │ │ └── CodeAnalysis │ │ ├── AnalyzerConfigOptionsExtensions.cs │ │ ├── AttributeExtensions.cs │ │ └── TypeSymbolExtensions.cs └── DotNetCampus.CommandLine │ ├── CommandLine.cs │ ├── CommandLineParsingOptions.cs │ ├── CommandLinePropertyValue.cs │ ├── CommandLineStyle.cs │ ├── CommandRunner.cs │ ├── CommandRunnerBuilderExtensions.cs │ ├── Compiler │ ├── CollectCommandHandlersFromThisAssemblyAttribute.cs │ ├── CommandLineAttribute.cs │ ├── CommandObjectCreator.cs │ ├── ICommandHandlerCollection.cs │ ├── OptionAttribute.cs │ ├── ValueAttribute.cs │ └── VerbAttribute.cs │ ├── DotNetCampus.CommandLine.csproj │ ├── Exceptions │ ├── CommandLineException.cs │ ├── CommandLineParseException.cs │ ├── CommandVerbAmbiguityException.cs │ ├── CommandVerbNotFoundException.cs │ └── RequiredPropertyNotAssignedException.cs │ ├── ICommandHandler.cs │ ├── ICommandRunnerBuilder.cs │ ├── Package │ └── build │ │ └── Package.props │ └── Utils │ ├── Collections │ ├── OptionDictionary.cs │ ├── ReadOnlyListRange.cs │ └── SingleOptimizedList.cs │ ├── CommandLineConverter.cs │ ├── Handlers │ ├── DictionaryCommandHandlerCollection.cs │ └── TaskCommandHandler.cs │ ├── NamingHelper.cs │ └── Parsers │ ├── CommandLineParsedResult.cs │ ├── DotNetStyleParser.cs │ ├── FlexibleStyleParser.cs │ ├── GnuStyleParser.cs │ ├── ICommandLineParser.cs │ ├── PosixStyleParser.cs │ ├── PowerShellStyleParser.cs │ └── UrlStyleParser.cs └── tests ├── DotNetCampus.CommandLine.Performance ├── CommandLineParserTest.cs ├── DotNetCampus.CommandLine.Performance.csproj ├── Fakes │ └── ComparedOptions.cs ├── Program.cs └── dotnetCampus.CommandLine.Legacy.dll └── DotNetCampus.CommandLine.Tests ├── AddHandlerTests.cs ├── Analyzers └── OptionLongNameMustBePascalCaseAnalyzerTest.cs ├── CommandLineTests.ValueRange.cs ├── CommandLineTests.bak.cs ├── DotNetCampus.CommandLine.Tests.csproj ├── DotNetCommandLineParserTests.cs ├── Fakes ├── AmbiguousOptions.cs ├── AmbiguousOptionsParser.cs ├── AssemblyCommandHandler.cs ├── CollectionOptions.cs ├── CommandLineArgs.cs ├── DefaultVerbCommandHandler.cs ├── DictionaryOptions.cs ├── FakeCommandOptions.cs ├── FakeVerbCommandHandler.cs ├── Options.cs ├── OptionsParser.cs ├── PrimaryOptions.cs ├── RuntimeImmutableOptions.cs ├── RuntimeOptions.cs ├── UnlimitedValueOptions.cs ├── ValueOptions.cs └── VerbOptions.cs ├── FlexibleCommandLineParserTests.cs ├── GnuCommandLineParserTests.cs ├── InheritanceCommandLineParserTests.cs ├── LogLevel.cs ├── PosixCommandLineParserTests.cs ├── PowerShellCommandLineParserTests.cs ├── UrlCommandLineParserTests.cs └── Utils └── NamingHelperTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: | 16 | 3.1.x 17 | 6.0.x 18 | 8.0.x 19 | 20 | - name: Build 21 | run: dotnet build --configuration Release 22 | 23 | - name: Test 24 | run: dotnet test --configuration Release 25 | 26 | - name: Pack 27 | run: dotnet pack --configuration Release --no-build 28 | -------------------------------------------------------------------------------- /.github/workflows/nuget-tag-publish.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: | 20 | 3.1.x 21 | 6.0.x 22 | 8.0.x 23 | 24 | - name: Install dotnet tool 25 | run: dotnet tool install -g dotnetCampus.TagToVersion 26 | 27 | - name: Set tag to version 28 | run: dotnet TagToVersion -t ${{ github.ref }} 29 | 30 | - name: Build with dotnet 31 | run: | 32 | dotnet build --configuration Release 33 | dotnet pack --configuration Release --no-build 34 | 35 | - name: Install Nuget 36 | uses: nuget/setup-nuget@v1 37 | with: 38 | nuget-version: '6.x' 39 | 40 | - name: Add private GitHub registry to NuGet 41 | run: | 42 | nuget sources add -name github -Source https://nuget.pkg.github.com/dotnet-campus/index.json -Username dotnet-campus -Password ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Push generated package to GitHub registry 45 | run: | 46 | nuget push .\artifacts\package\release\*.nupkg -Source github -SkipDuplicate 47 | nuget push .\artifacts\package\release\*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -ApiKey ${{ secrets.NugetKey }} 48 | -------------------------------------------------------------------------------- /ApiChanges.3.x-4.0.md: -------------------------------------------------------------------------------- 1 | # 重大更新 2 | 3 | ## 3.x -> 4.0 4 | 5 | DotNetCampus.CommandLine 4.0版本带来了全面的架构升级和功能增强,使命令行参数解析更加高效、灵活且易于使用。 6 | 7 | ### 主要更新内容 8 | 9 | #### 1. 全面采用源代码生成器 10 | 11 | - 完全使用源生成器技术,替代以前的手写解析器(`XxxParser`)和反射解析器(`RuntimeParser`) 12 | - 相比上个版本,性能出现了轻微下降(换来了各种命令行风格的完全支持),好在保持在同一个数量级内,仍然比同类库快得多 13 | - 充分支持 AOT 编译,更适合现代 .NET 应用场景 14 | 15 | #### 2. 全面增强的命令行风格支持 16 | 17 | - 从原来支持 Linux/GNU、CMD、PowerShell 三种风格的有限子集,升级为全面支持五种完整命令行风格: 18 | - `CommandLineStyle.Flexible`(默认):智能识别多种风格 19 | - `CommandLineStyle.Gnu`:符合 GNU 规范的风格 20 | - `CommandLineStyle.Posix`:符合 POSIX 规范的风格 21 | - `CommandLineStyle.DotNet`:.NET CLI 风格 22 | - `CommandLineStyle.PowerShell`:PowerShell 风格 23 | - 全面支持各种命令行选项格式: 24 | - 长选项:`--option value`、`--option=value`、`--option:value` 25 | - 短选项:`-o value`、`-o=value`、`-o:value` 26 | - Windows风格:`/option value`、`/option:value` 27 | - 布尔选项:`--flag`(无值时自动设为true)、`--flag:true/false` 28 | - URL协议风格:`protocol://command/subcommand?param1=value¶m2=value` 29 | 30 | #### 3. 增强的类型系统支持 31 | 32 | - 支持 C# 8.0+ 的 `init` 和 C# 11.0 的 `required` 关键字,支持不可变对象 33 | - 全面支持更多复杂数据类型: 34 | - 集合类型:数组、列表、只读集合、不可变集合 35 | - 字典类型:支持多种传入方式和分隔符 36 | - 枚举类型:支持枚举名称和数值 37 | - 增强类型转换能力,提供更友好的类型错误提示 38 | 39 | #### 4. 改进的命令处理器模式 40 | 41 | - `AddHandler` 现在支持实现 `ICommandHandler` 接口的类型,可以在类型内部编写处理逻辑 42 | - 保留了通过委托方式传入处理逻辑的经典用法 43 | 44 | ### 破坏性变更和升级指南 45 | 46 | 1. **命名空间变更**:`OptionAttribute`、`ValueAttribute`、`VerbAttribute` 的命名空间发生了变化。升级库后,您可能需要借助IDE来修正相关引用。 47 | 48 | 2. **参数解析选项变更**:`CommandLine.Parse(args, xxx)` 的第二个参数已从简单的URL scheme字符串升级为完整的 `CommandLineParsingOptions` 对象: 49 | ```csharp 50 | // 旧版本 51 | var commandLine = CommandLine.Parse(args, "myapp"); 52 | 53 | // 新版本 54 | var commandLine = CommandLine.Parse(args, new CommandLineParsingOptions { SchemeNames = ["myapp"] }); 55 | // 或者使用预定义样式 56 | var commandLine = CommandLine.Parse(args, CommandLineParsingOptions.DotNet); 57 | ``` 58 | 59 | 3. **命名约定变更**:选项命名规则从 `PascalCase` 改为推荐使用 `kebab-case`: 60 | ```csharp 61 | // 旧版本(依然支持,但不再推荐) 62 | [Option("OutputFile")] 63 | 64 | // 新版本(推荐) 65 | [Option("output-file")] 66 | ``` 67 | 原因请见:[DCL101.md](/docs/analyzers/DCL101.md) 68 | 69 | 4. **标准处理器移除**:删除了 `AddStandardHandlers` 方法(用于自动处理 `--help` 和 `--version`)。这个功能在未来版本可能会以新形式回归,如有需要请暂时自行实现。 70 | 71 | 5. **过滤器机制移除**:删除了很少使用的 `CommandLineFilter` 机制,该机制主要为 `AddStandardHandlers` 提供支持。 72 | 73 | ### 保留的功能 74 | 75 | 本次升级范围很大,不过仍然尽量在更规范更强大的设计中保持了跟上个版本 API 的一致性。你原来的项目通常只进行命名空间的修改即可正常工作;调整选项的命名规则后可消除新引入的警告(我们提供了代码修改器辅助你自动采用新的命名规则)。 76 | 77 | 1. **多种命令行风格解析**:保留并增强了对多种命令行参数风格的支持 78 | 2. **URL协议支持**:完整保留并改进了对URL协议格式命令行的解析 79 | 3. **位置参数支持**:保留了对位置参数的支持,并增强了位置参数的处理能力 80 | 4. **谓词(命令)支持**:保留并增强了对命令谓词的支持(如 `git commit`、`git push` 等形式) 81 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | latest 8 | enable 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)artifacts\ 12 | 13 | 14 | 15 | 18 | $(NoWarn);DCL101 19 | 22 | $(WarningAsErrors);CA1416 23 | 24 | 25 | 26 | 27 | 使用源生成器高性能地辅助你的应用程序解析几种主流风格的命令行。 28 | walterlv 29 | dotnet-campus 30 | MIT 31 | https://github.com/dotnet-campus/DotNetCampus.CommandLine 32 | https://github.com/dotnet-campus/DotNetCampus.CommandLine.git 33 | git 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /DotNetCampus.CommandLine.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31606.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{2AC77BF2-2FBB-4DE2-9444-38EC30003867}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | Directory.Build.props = Directory.Build.props 11 | LICENSE = LICENSE 12 | README.md = README.md 13 | build\Version.props = build\Version.props 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCampus.CommandLine.Analyzer", "src\DotNetCampus.CommandLine.Analyzer\DotNetCampus.CommandLine.Analyzer.csproj", "{8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7DDA8183-3606-4B08-86E3-A4537860448F}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F26EAA27-AA79-4B28-890C-D759F1D1A374}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCampus.CommandLine", "src\DotNetCampus.CommandLine\DotNetCampus.CommandLine.csproj", "{B61424A0-02C5-4C24-819B-8153D52BC0B8}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCampus.CommandLine.Sample", "samples\DotNetCampus.CommandLine.Sample\DotNetCampus.CommandLine.Sample.csproj", "{6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCampus.CommandLine.Tests", "tests\DotNetCampus.CommandLine.Tests\DotNetCampus.CommandLine.Tests.csproj", "{70991994-BB0C-4D00-9B74-E8736D0AD7C1}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCampus.CommandLine.Performance", "tests\DotNetCampus.CommandLine.Performance\DotNetCampus.CommandLine.Performance.csproj", "{56B65FC1-CE75-4981-A880-954D891901D6}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Debug|x86 = Debug|x86 34 | Release|Any CPU = Release|Any CPU 35 | Release|x86 = Release|x86 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Debug|x86.Build.0 = Debug|Any CPU 42 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Release|x86.ActiveCfg = Release|Any CPU 45 | {8AD0FEAB-2E36-4EBB-9E32-C4394FC6DC86}.Release|x86.Build.0 = Release|Any CPU 46 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Debug|x86.ActiveCfg = Debug|Any CPU 49 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Debug|x86.Build.0 = Debug|Any CPU 50 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Release|x86.ActiveCfg = Release|Any CPU 53 | {B61424A0-02C5-4C24-819B-8153D52BC0B8}.Release|x86.Build.0 = Release|Any CPU 54 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Debug|x86.Build.0 = Debug|Any CPU 58 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Release|x86.ActiveCfg = Release|Any CPU 61 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8}.Release|x86.Build.0 = Release|Any CPU 62 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Debug|x86.ActiveCfg = Debug|Any CPU 65 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Debug|x86.Build.0 = Debug|Any CPU 66 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Release|x86.ActiveCfg = Release|Any CPU 69 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1}.Release|x86.Build.0 = Release|Any CPU 70 | {56B65FC1-CE75-4981-A880-954D891901D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {56B65FC1-CE75-4981-A880-954D891901D6}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {56B65FC1-CE75-4981-A880-954D891901D6}.Debug|x86.ActiveCfg = Debug|Any CPU 73 | {56B65FC1-CE75-4981-A880-954D891901D6}.Debug|x86.Build.0 = Debug|Any CPU 74 | {56B65FC1-CE75-4981-A880-954D891901D6}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {56B65FC1-CE75-4981-A880-954D891901D6}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {56B65FC1-CE75-4981-A880-954D891901D6}.Release|x86.ActiveCfg = Release|Any CPU 77 | {56B65FC1-CE75-4981-A880-954D891901D6}.Release|x86.Build.0 = Release|Any CPU 78 | EndGlobalSection 79 | GlobalSection(SolutionProperties) = preSolution 80 | HideSolutionNode = FALSE 81 | EndGlobalSection 82 | GlobalSection(NestedProjects) = preSolution 83 | {6B8F7500-B161-408D-BFA3-AE77CB8CF4D8} = {F26EAA27-AA79-4B28-890C-D759F1D1A374} 84 | {70991994-BB0C-4D00-9B74-E8736D0AD7C1} = {7DDA8183-3606-4B08-86E3-A4537860448F} 85 | {56B65FC1-CE75-4981-A880-954D891901D6} = {7DDA8183-3606-4B08-86E3-A4537860448F} 86 | EndGlobalSection 87 | GlobalSection(ExtensibilityGlobals) = postSolution 88 | SolutionGuid = {52E30C59-C5C8-4517-811A-667BFA2BFABB} 89 | EndGlobalSection 90 | EndGlobal 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 dotnet campus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotNetCampus.CommandLine 2 | 3 | ![Build](https://github.com/dotnet-campus/DotNetCampus.CommandLine/actions/workflows/dotnet-build.yml/badge.svg) ![NuGet Package](https://github.com/dotnet-campus/DotNetCampus.CommandLine/actions/workflows/nuget-tag-publish.yml/badge.svg) [![DotNetCampus.CommandLine](https://img.shields.io/nuget/v/DotNetCampus.CommandLine.svg?label=DotnetCampus.CommandLine)](https://www.nuget.org/packages/DotnetCampus.CommandLine/) [![dotnetCampus.CommandLine.Source](https://img.shields.io/nuget/v/DotnetCampus.CommandLine.Source?label=DotnetCampus.CommandLine.Source)](https://www.nuget.org/packages/DotnetCampus.CommandLine.Source/) 4 | 5 | | [English][en] | [简体中文][zh-hans] | [繁體中文][zh-hant] | 6 | | ------------- | ------------------- | ------------------- | 7 | 8 | [en]: /docs/en/README.md 9 | [zh-hans]: /docs/zh-hans/README.md 10 | [zh-hant]: /docs/zh-hant/README.md 11 | 12 | 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. 13 | 14 | Parsing a typical command line takes only about 0.8μs (microseconds), making it one of the fastest command line parsers available in .NET. 15 | 16 | ## Get Started 17 | 18 | For your program `Main` method, write this code: 19 | 20 | ```csharp 21 | class Program 22 | { 23 | static void Main(string[] args) 24 | { 25 | // Create a new instance of CommandLine type from command line arguments 26 | var commandLine = CommandLine.Parse(args); 27 | 28 | // Parse the command line into an instance of Options type 29 | // The source generator will automatically handle the parsing for you 30 | var options = commandLine.As(); 31 | 32 | // Now use your options object to implement your functionality 33 | } 34 | } 35 | ``` 36 | 37 | Define a class that maps command line arguments: 38 | 39 | ```csharp 40 | class Options 41 | { 42 | [Value(0)] 43 | public required string FilePath { get; init; } 44 | 45 | [Option('s', "silence")] 46 | public bool IsSilence { get; init; } 47 | 48 | [Option('m', "mode")] 49 | public string? StartMode { get; init; } 50 | 51 | [Option("startup-sessions")] 52 | public IReadOnlyList StartupSessions { get; init; } = []; 53 | } 54 | ``` 55 | 56 | Then use different command line styles to populate instances of this type: 57 | 58 | ### Windows PowerShell Style 59 | 60 | ```powershell 61 | > demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s -Mode Edit -StartupSessions A B C 62 | ``` 63 | 64 | ### Windows CMD Style 65 | 66 | ```cmd 67 | > demo.exe "C:\Users\lvyi\Desktop\demo.txt" /s /Mode Edit /StartupSessions A B C 68 | ``` 69 | 70 | ### Linux/GNU Style 71 | 72 | ```bash 73 | $ demo.exe "C:/Users/lvyi/Desktop/demo.txt" -s --mode Edit --startup-sessions A --startup-sessions B --startup-sessions C 74 | ``` 75 | 76 | ### .NET CLI Style 77 | ``` 78 | > demo.exe "C:\Users\lvyi\Desktop\demo.txt" -s:true --mode:Edit --startup-sessions:A;B;C 79 | ``` 80 | 81 | ## Command Styles and Features 82 | 83 | The library supports multiple command line styles through `CommandLineStyle` enum: 84 | - Flexible (default): Intelligently recognizes multiple styles 85 | - GNU: GNU standard compliant 86 | - POSIX: POSIX standard compliant 87 | - DotNet: .NET CLI style 88 | - PowerShell: PowerShell style 89 | 90 | Advanced features include: 91 | - Support for various data types including collections and dictionaries 92 | - Positional arguments with `ValueAttribute` 93 | - Required properties with C# `required` modifier 94 | - Command handling with verb support 95 | - URL protocol parsing 96 | - High performance thanks to source generators 97 | 98 | ## Engage, Contribute and Provide Feedback 99 | 100 | Thank you very much for firing a new issue and providing new pull requests. 101 | 102 | ### Issue 103 | 104 | Click here to file a new issue: 105 | 106 | - [New Issue · dotnet-campus/DotNetCampus.CommandLine](https://github.com/dotnet-campus/DotNetCampus.CommandLine/issues/new) 107 | 108 | ### Contributing Guide 109 | 110 | Be kindly. 111 | 112 | ## License 113 | 114 | DotNetCampus.CommandLine is licensed under the [MIT license](/LICENSE). 115 | -------------------------------------------------------------------------------- /build/Version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0-alpha01 4 | 5 | -------------------------------------------------------------------------------- /docs/analyzers/DCL101.md: -------------------------------------------------------------------------------- 1 | # DCL101 2 | 3 | ## Option long name should be kebab-case 4 | 5 | Option long name should be kebab-case 6 | 7 | 建议命令行选项的长名称使用 kebab-case 命名法,即使你可以在命令行环境中使用任何风格。 8 | 9 | ## Code 10 | 11 | ```csharp 12 | class Options 13 | { 14 | [Option('d', "walterlv-is-adobe")] 15 | public string? DemoOption { get; set; } 16 | } 17 | ``` 18 | 19 | In this case, `walterlv-is-adobe` is the recommended format. 20 | 21 | 在这个例子中,`walterlv-is-adobe` 是推荐的格式。 22 | 23 | ```csharp 24 | class Options 25 | { 26 | [Option('d', "WalterlvIsAdobe")] 27 | public string? DemoOption { get; set; } 28 | } 29 | ``` 30 | 31 | In this case, `WalterlvIsAdobe` may be reported with a suggestion to change to `walterlv-is-adobe`. 32 | 33 | 在这个例子中,`WalterlvIsAdobe` 可能会被报告并建议更改为 `walterlv-is-adobe`。 34 | 35 | ## Why 36 | 37 | The DotNetCampus.CommandLine supports various kinds of command-line styles. Users can type commands using PowerShell style, Bash style, CMD style, or even web URL style. We recommend kebab-case naming convention because: 38 | 39 | 1. It provides more word-splitting information. For example, with a name like "DotNetCampus", if written in PascalCase as "DotNetCampus", it would automatically match "DotNetCampus" or "dot-net-campus". However, if written in kebab-case as "dotnet-campus", it would match "DotNetCampus", "dotnet-campus", or even variations with different capitalization within words. 40 | 41 | 2. It clarifies number association problems. For example, is "Foo2Bar" meant to be "Foo2-Bar" or "Foo-2-Bar"? Semantically we might interpret "Version2Info" as "Version2-Info", but this can't be algorithmically distinguished from the previous case. Kebab-case provides clear word boundaries and shows which number belongs to which word. 42 | 43 | 3. Since our command-line library supports multiple styles of parameter input, using kebab-case helps avoid ambiguity, especially since we support case-sensitive kebab-case (although it's case-insensitive by default). 44 | 45 | DotNetCampus.CommandLine 支持多种命令行风格。用户可以使用 PowerShell 风格、Bash 风格、CMD 风格甚至网址风格输入命令。我们推荐 kebab-case 命名约定的原因是: 46 | 47 | 1. 它提供了更多的单词拆分信息。例如,对于名称 "DotNetCampus",如果按 PascalCase 命名法写为 "DotNetCampus",那么会自动匹配 "DotNetCampus" 或 "dot-net-campus";而如果按 kebab-case 命名法写为 "dotnet-campus",则会匹配 "DotNetCampus"、"dotnet-campus" 或带有单词内部不同大小写的变体。 48 | 49 | 2. 它解决了数字从属问题。例如 "Foo2Bar" 到底是 "Foo2-Bar" 还是 "Foo-2-Bar"?从语义上我们可能会将 "Version2Info" 解读为 "Version2-Info",但这无法与前面的情况在算法上进行区分。Kebab-case 提供了清晰的单词边界,并显示了数字属于哪个单词。 50 | 51 | 3. 由于我们的命令行库支持多种不同风格的参数输入,使用 kebab-case 可以更好地避免歧义,尤其是我们支持大小写敏感的 kebab-case(虽然默认是大小写不敏感的)。 52 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/DefaultOptions.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Compiler; 2 | using DotNetCampus.Cli.Properties; 3 | 4 | #pragma warning disable CS0618 // 类型或成员已过时 5 | 6 | namespace DotNetCampus.Cli; 7 | 8 | internal class DefaultOptions 9 | { 10 | [Option(LocalizableDescription = nameof(LocalizableStrings.SamplePropertyDescription))] 11 | public string? DefaultText { get; set; } 12 | 13 | [Option(LocalizableDescription = nameof(LocalizableStrings.SampleDirectoryPropertyDescription))] 14 | public string? DefaultDirectory { get; set; } 15 | 16 | internal void Run() 17 | { 18 | Console.WriteLine("默认行为执行……"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | DotNetCampus.Cli 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ..\..\tests\dotnetCampus.CommandLine.Performance\dotnetCampus.CommandLine.Legacy.dll 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | #define Benchmark 2 | using System.Data.Common; 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | using DotNetCampus.Cli.Compiler; 6 | using DotNetCampus.Cli.Tests.Fakes; 7 | 8 | namespace DotNetCampus.Cli; 9 | 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | #if Benchmark 15 | // 第一次运行,排除类型初始化的影响,只测试代码执行性能。 16 | // 注释掉这句话,可以: 17 | // 1. 测试带类型初始化的性能 18 | // 2. 测试 AOT 性能 dotnet publish --self-contained -r win-x64 -c release -tl:off .\src\DotNetCampus.CommandLine.Sample\DotNetCampus.CommandLine.Sample.csproj 19 | Run(args); 20 | var stopwatch = Stopwatch.StartNew(); 21 | Run(args); 22 | stopwatch.Stop(); 23 | Console.WriteLine($"[# Elapsed: {stopwatch.Elapsed.TotalMicroseconds} us #]"); 24 | #else 25 | const int testCount = 1000000; 26 | CommandLineParsingOptions parsingOptions = CommandLineParsingOptions.DotNet; 27 | 28 | for (var i = 0; i < testCount; i++) 29 | { 30 | dotnetCampus.Cli.CommandLine.Parse(args).As(new OptionsParser()); 31 | dotnetCampus.Cli.CommandLine.Parse(args).As(); 32 | _ = CommandLine.Parse(args, parsingOptions).As(); 33 | } 34 | 35 | var stopwatch = new Stopwatch(); 36 | 37 | Console.WriteLine($"Run {testCount} times for: {string.Join(" ", args)}"); 38 | 39 | Console.WriteLine("| Version | Parse | As(Parser) | As(Runtime) |"); 40 | Console.WriteLine("| ------- | ------- | ---------- | ----------- |"); 41 | 42 | Console.Write("| 3.x | "); 43 | stopwatch.Restart(); 44 | for (var i = 0; i < testCount; i++) 45 | { 46 | _ = dotnetCampus.Cli.CommandLine.Parse(args); 47 | } 48 | stopwatch.Stop(); 49 | Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); 50 | var oldCommandLine = dotnetCampus.Cli.CommandLine.Parse(args); 51 | stopwatch.Restart(); 52 | for (var i = 0; i < testCount; i++) 53 | { 54 | _ = oldCommandLine.As(new OptionsParser()); 55 | } 56 | stopwatch.Stop(); 57 | Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); 58 | stopwatch.Restart(); 59 | for (var i = 0; i < testCount; i++) 60 | { 61 | _ = oldCommandLine.As(); 62 | } 63 | stopwatch.Stop(); 64 | Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); 65 | 66 | Console.Write("| 4.x | "); 67 | stopwatch.Restart(); 68 | for (var i = 0; i < testCount; i++) 69 | { 70 | _ = CommandLine.Parse(args, parsingOptions); 71 | } 72 | stopwatch.Stop(); 73 | Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),4} ms | "); 74 | var newCommandLine = CommandLine.Parse(args, parsingOptions); 75 | stopwatch.Restart(); 76 | for (var i = 0; i < testCount; i++) 77 | { 78 | _ = newCommandLine.As(OptionsBuilder.CreateInstance); 79 | } 80 | stopwatch.Stop(); 81 | Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); 82 | stopwatch.Restart(); 83 | for (var i = 0; i < testCount; i++) 84 | { 85 | _ = newCommandLine.As(); 86 | } 87 | stopwatch.Stop(); 88 | Console.WriteLine($"{stopwatch.ElapsedMilliseconds.ToString(),8} ms |"); 89 | #endif 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | private static void Run(string[] args) 94 | { 95 | if (args.Length is 0) 96 | { 97 | } 98 | else if (args[0] == "3.x-parser") 99 | { 100 | Run3xParser(args); 101 | } 102 | else if (args[0] == "3.x-runtime") 103 | { 104 | Run3xRuntime(args); 105 | } 106 | else if (args[0] == "4.x-interceptor") 107 | { 108 | Run4xInterceptor(args); 109 | } 110 | else if (args[0] == "4.x-module") 111 | { 112 | Run4xModule(args); 113 | } 114 | } 115 | 116 | [MethodImpl(MethodImplOptions.NoInlining)] 117 | private static void Run3xParser(string[] args) 118 | { 119 | _ = dotnetCampus.Cli.CommandLine.Parse(args).As(new OptionsParser()); 120 | } 121 | 122 | [MethodImpl(MethodImplOptions.NoInlining)] 123 | private static void Run3xRuntime(string[] args) 124 | { 125 | _ = dotnetCampus.Cli.CommandLine.Parse(args).As(); 126 | } 127 | 128 | [MethodImpl(MethodImplOptions.NoInlining)] 129 | private static void Run4xInterceptor(string[] args) 130 | { 131 | _ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); 132 | } 133 | 134 | [MethodImpl(MethodImplOptions.NoInlining)] 135 | private static void Run4xModule(string[] args) 136 | { 137 | Initialize(); 138 | _ = CommandLine.Parse(args, CommandLineParsingOptions.DotNet).As(); 139 | } 140 | 141 | [MethodImpl(MethodImplOptions.NoInlining)] 142 | internal static void Initialize() 143 | { 144 | // DefaultOptions { VerbName = null } 145 | global::DotNetCampus.Cli.CommandRunner.Register( 146 | null, 147 | global::DotNetCampus.Cli.DefaultOptionsBuilder.CreateInstance); 148 | 149 | // EditOptions { VerbName = "Edit" } 150 | global::DotNetCampus.Cli.CommandRunner.Register( 151 | "Edit", 152 | global::DotNetCampus.Cli.Tests.Fakes.EditOptionsBuilder.CreateInstance); 153 | 154 | // Options { VerbName = null } 155 | global::DotNetCampus.Cli.CommandRunner.Register( 156 | null, 157 | global::DotNetCampus.Cli.Tests.Fakes.OptionsBuilder.CreateInstance); 158 | 159 | // PrintOptions { VerbName = "Print" } 160 | global::DotNetCampus.Cli.CommandRunner.Register( 161 | "Print", 162 | global::DotNetCampus.Cli.Tests.Fakes.PrintOptionsBuilder.CreateInstance); 163 | 164 | // SampleCommandHandler { VerbName = "sample" } 165 | global::DotNetCampus.Cli.CommandRunner.Register( 166 | "sample", 167 | global::DotNetCampus.Cli.SampleCommandHandlerBuilder.CreateInstance); 168 | 169 | // SampleOptions { VerbName = "sample-options" } 170 | global::DotNetCampus.Cli.CommandRunner.Register( 171 | "sample-options", 172 | global::DotNetCampus.Cli.SampleOptionsBuilder.CreateInstance); 173 | 174 | // ShareOptions { VerbName = "Share" } 175 | global::DotNetCampus.Cli.CommandRunner.Register( 176 | "Share", 177 | global::DotNetCampus.Cli.Tests.Fakes.ShareOptionsBuilder.CreateInstance); 178 | } 179 | } 180 | 181 | // [CollectCommandHandlersFromThisAssembly] 182 | // internal partial class AssemblyCommandHandler; 183 | 184 | [Verb("sample")] 185 | internal class SampleCommandHandler : ICommandHandler 186 | { 187 | [Option("SampleProperty")] 188 | public required string Option { get; init; } 189 | 190 | [Value(Length = int.MaxValue)] 191 | public string? Argument { get; init; } 192 | 193 | public Task RunAsync() 194 | { 195 | Console.WriteLine($"Option: {Option}"); 196 | Console.WriteLine($"Argument: {Argument}"); 197 | return Task.FromResult(0); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/LocalizableStrings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DotNetCampus.Cli.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class LocalizableStrings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal LocalizableStrings() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetCampus.Cli.Properties.LocalizableStrings", typeof(LocalizableStrings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性 51 | /// 重写当前线程的 CurrentUICulture 属性。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 Pass any directory into this option. 的本地化字符串。 65 | /// 66 | internal static string SampleDirectoryPropertyDescription { 67 | get { 68 | return ResourceManager.GetString("SampleDirectoryPropertyDescription", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// 查找类似 Pass any file into this option. 的本地化字符串。 74 | /// 75 | internal static string SampleFilePropertyDescription { 76 | get { 77 | return ResourceManager.GetString("SampleFilePropertyDescription", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// 查找类似 Output any text passed to this option. 的本地化字符串。 83 | /// 84 | internal static string SamplePropertyDescription { 85 | get { 86 | return ResourceManager.GetString("SamplePropertyDescription", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// 查找类似 Use sample command line action to output some text. 的本地化字符串。 92 | /// 93 | internal static string SampleVerbDescription { 94 | get { 95 | return ResourceManager.GetString("SampleVerbDescription", resourceCulture); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/LocalizableStrings.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Pass any directory into this option. 122 | 123 | 124 | Pass any file into this option. 125 | 126 | 127 | Output any text passed to this option. 128 | 129 | 130 | Use sample command line action to output some text. 131 | 132 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/LocalizableStrings.zh-CN.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 允许传入一个目录路径。 122 | 123 | 124 | 允许传入一个文件路径。 125 | 126 | 127 | 输出传入的任何类型的字符串。 128 | 129 | 130 | 执行示例命令。 131 | 132 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/LocalizableStrings.zh-CN.zh-hans-cn.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | text/microsoft-resx 4 | 5 | 6 | 1.3 7 | 8 | 9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 10 | 11 | 12 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 13 | 14 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/LocalizableStrings.zh-hans-cn.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | text/microsoft-resx 4 | 5 | 6 | 1.3 7 | 8 | 9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 10 | 11 | 12 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 13 | 14 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Debug": { 4 | "commandName": "Project" 5 | }, 6 | "Debug --help": { 7 | "commandName": "Project", 8 | "commandLineArgs": "--help" 9 | }, 10 | "Debug --version": { 11 | "commandName": "Project", 12 | "commandLineArgs": "--version" 13 | }, 14 | "Debug sample": { 15 | "commandName": "Project", 16 | "commandLineArgs": "sample --sample-property \"Hello World\"" 17 | }, 18 | "Debug sample --help": { 19 | "commandName": "Project", 20 | "commandLineArgs": "sample --help" 21 | }, 22 | "Debug unknown": { 23 | "commandName": "Project", 24 | "commandLineArgs": "unknown --sample-property \"Hello World\"" 25 | }, 26 | "Debug unknown --help": { 27 | "commandName": "Project", 28 | "commandLineArgs": "unknown --help" 29 | }, 30 | "Debug --options": { 31 | "commandName": "Project", 32 | "commandLineArgs": "--sample-property \"Hello World\"" 33 | }, 34 | "Debug --many-options": { 35 | "commandName": "Project", 36 | "commandLineArgs": "C:\\Users\\lvyi\\Desktop\\文件.txt --cloud --iwb -m Display -s -p Outside --startup-session 89EA9D26-6464-4E71-BD04-AA6516063D83" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/DotNetCampus.CommandLine.Sample/SampleOptions.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Compiler; 2 | using DotNetCampus.Cli.Properties; 3 | 4 | #pragma warning disable CS0618 // 类型或成员已过时 5 | 6 | namespace DotNetCampus.Cli; 7 | 8 | [Verb("sample-options", LocalizableDescription = nameof(LocalizableStrings.SampleVerbDescription))] 9 | internal class SampleOptions 10 | { 11 | [Option(LocalizableDescription = nameof(LocalizableStrings.SamplePropertyDescription))] 12 | public string? SampleText { get; set; } 13 | 14 | [Option(LocalizableDescription = nameof(LocalizableStrings.SampleFilePropertyDescription))] 15 | public string? SampleFile { get; set; } 16 | 17 | internal void Run() 18 | { 19 | Console.WriteLine("示例行为执行……"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 4.0 5 | 6 | ### New Rules 7 | Rule ID | Category | Severity | Notes 8 | --------|----------|----------|------- 9 | DCL101 | DotNetCampus.AvoidBugs | Warning | 10 | DCL201 | DotNetCampus.CodeFixOnly | Hidden | 11 | DCL202 | DotNetCampus.RuntimeException | Error | 12 | 13 | ## Release 3.2 14 | 15 | ### New Rules 16 | Rule ID | Category | Severity | Notes 17 | --------|----------|----------|------- 18 | DCL101 | dotnetCampus.Naming | Error | 19 | DCL201 | dotnetCampus.Usage | Hidden | 20 | DCL202 | dotnetCampus.Usage | Error | 21 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CodeActions; 3 | using Microsoft.CodeAnalysis.CodeFixes; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | 6 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 7 | 8 | public abstract class ConvertOptionPropertyTypeCodeFix : CodeFixProvider 9 | { 10 | public sealed override FixAllProvider GetFixAllProvider() 11 | { 12 | // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers 13 | return WellKnownFixAllProviders.BatchFixer; 14 | } 15 | 16 | protected abstract string CodeActionTitle { get; } 17 | 18 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 19 | { 20 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 21 | if (root is null) 22 | { 23 | return; 24 | } 25 | 26 | var diagnostic = context.Diagnostics.First(); 27 | var diagnosticSpan = diagnostic.Location.SourceSpan; 28 | 29 | if (root.FindNode(diagnosticSpan) is TypeSyntax typeSyntax) 30 | { 31 | context.RegisterCodeFix( 32 | CodeAction.Create( 33 | title: CodeActionTitle, 34 | createChangedSolution: c => ConvertPropertyTypeAsync(context.Document, typeSyntax, c), 35 | equivalenceKey: GetType().FullName), 36 | diagnostic); 37 | } 38 | } 39 | 40 | private async Task ConvertPropertyTypeAsync(Document document, TypeSyntax typeSyntax, CancellationToken cancellationToken) 41 | { 42 | var root = (CompilationUnitSyntax?)await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 43 | var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); 44 | if (root is null || semanticModel is null) 45 | { 46 | return document.Project.Solution; 47 | } 48 | 49 | var newRoot = CreateTypeSyntaxNode(typeSyntax, root, semanticModel, cancellationToken); 50 | return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, newRoot); 51 | } 52 | 53 | protected abstract CompilationUnitSyntax CreateTypeSyntaxNode( 54 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 55 | CancellationToken cancellationToken); 56 | } 57 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToBooleanCodeFix)), Shared] 12 | public class OptionPropertyTypeToBooleanCodeFix : ConvertOptionPropertyTypeCodeFix 13 | { 14 | public sealed override ImmutableArray FixableDiagnosticIds => 15 | [ 16 | Diagnostics.SupportedOptionPropertyType, 17 | Diagnostics.NotSupportedOptionPropertyType, 18 | ]; 19 | 20 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToBoolean; 21 | 22 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 23 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 24 | CancellationToken cancellationToken) 25 | { 26 | return syntaxRoot.ReplaceNode( 27 | oldTypeSyntax, 28 | SyntaxFactory.PredefinedType( 29 | SyntaxFactory.Token(SyntaxKind.BoolKeyword))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Simplification; 9 | 10 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 11 | 12 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToDictionaryCodeFix)), Shared] 13 | public class OptionPropertyTypeToDictionaryCodeFix : ConvertOptionPropertyTypeCodeFix 14 | { 15 | public sealed override ImmutableArray FixableDiagnosticIds => 16 | [ 17 | Diagnostics.SupportedOptionPropertyType, 18 | Diagnostics.NotSupportedOptionPropertyType, 19 | ]; 20 | 21 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToDictionary; 22 | 23 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 24 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 25 | CancellationToken cancellationToken) 26 | { 27 | return syntaxRoot.ReplaceNode( 28 | oldTypeSyntax, 29 | SyntaxFactory.ParseName("global::System.Collections.Generic.IReadOnlyDictionary") 30 | .WithAdditionalAnnotations(Simplifier.Annotation)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToDoubleCodeFix)), Shared] 12 | public class OptionPropertyTypeToDoubleCodeFix : ConvertOptionPropertyTypeCodeFix 13 | { 14 | public sealed override ImmutableArray FixableDiagnosticIds => 15 | [ 16 | Diagnostics.SupportedOptionPropertyType, 17 | Diagnostics.NotSupportedOptionPropertyType, 18 | ]; 19 | 20 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToDouble; 21 | 22 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 23 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 24 | CancellationToken cancellationToken) 25 | { 26 | return syntaxRoot.ReplaceNode( 27 | oldTypeSyntax, 28 | SyntaxFactory.PredefinedType( 29 | SyntaxFactory.Token(SyntaxKind.DoubleKeyword))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToInt32CodeFix)), Shared] 12 | public class OptionPropertyTypeToInt32CodeFix : ConvertOptionPropertyTypeCodeFix 13 | { 14 | public sealed override ImmutableArray FixableDiagnosticIds => 15 | [ 16 | Diagnostics.SupportedOptionPropertyType, 17 | Diagnostics.NotSupportedOptionPropertyType, 18 | ]; 19 | 20 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToInt32; 21 | 22 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 23 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 24 | CancellationToken cancellationToken) 25 | { 26 | return syntaxRoot.ReplaceNode( 27 | oldTypeSyntax, 28 | SyntaxFactory.PredefinedType( 29 | SyntaxFactory.Token(SyntaxKind.IntKeyword))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Simplification; 9 | 10 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 11 | 12 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToListCodeFix)), Shared] 13 | public class OptionPropertyTypeToListCodeFix : ConvertOptionPropertyTypeCodeFix 14 | { 15 | public sealed override ImmutableArray FixableDiagnosticIds => 16 | [ 17 | Diagnostics.SupportedOptionPropertyType, 18 | Diagnostics.NotSupportedOptionPropertyType, 19 | ]; 20 | 21 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToList; 22 | 23 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 24 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 25 | CancellationToken cancellationToken) 26 | { 27 | return syntaxRoot.ReplaceNode( 28 | oldTypeSyntax, 29 | SyntaxFactory.ParseName("global::System.Collections.Generic.IReadOnlyList") 30 | .WithAdditionalAnnotations(Simplifier.Annotation)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using DotNetCampus.CommandLine.Properties; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToStringCodeFix)), Shared] 12 | public class OptionPropertyTypeToStringCodeFix : ConvertOptionPropertyTypeCodeFix 13 | { 14 | public sealed override ImmutableArray FixableDiagnosticIds => 15 | [ 16 | Diagnostics.SupportedOptionPropertyType, 17 | Diagnostics.NotSupportedOptionPropertyType, 18 | ]; 19 | 20 | protected sealed override string CodeActionTitle => Localizations.DCL201_202_Fix_OptionTypeToString; 21 | 22 | protected sealed override CompilationUnitSyntax CreateTypeSyntaxNode( 23 | TypeSyntax oldTypeSyntax, CompilationUnitSyntax syntaxRoot, SemanticModel semanticModel, 24 | CancellationToken cancellationToken) 25 | { 26 | return syntaxRoot.ReplaceNode( 27 | oldTypeSyntax, 28 | SyntaxFactory.PredefinedType( 29 | SyntaxFactory.Token(SyntaxKind.StringKeyword))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using DotNetCampus.Cli.Utils; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace DotNetCampus.CommandLine.Analyzers; 9 | 10 | /// 11 | /// [Option("LongName")] 12 | /// The LongName must be kebab-case. If not, this analyzer report diagnostics. 13 | /// 14 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 15 | public class OptionLongNameMustBeKebabCaseAnalyzer : DiagnosticAnalyzer 16 | { 17 | /// 18 | /// Recognize these attributes. 19 | /// 20 | private readonly ImmutableHashSet _attributeNames = ["Option", "OptionAttribute"]; 21 | 22 | /// 23 | /// Supported diagnostics. 24 | /// 25 | public override ImmutableArray SupportedDiagnostics => 26 | [ 27 | Diagnostics.DCL101_OptionLongNameMustBeKebabCase, 28 | ]; 29 | 30 | /// 31 | /// Register property analyzer. 32 | /// 33 | /// 34 | public override void Initialize(AnalysisContext context) 35 | { 36 | if (context is null) 37 | { 38 | throw new ArgumentNullException(nameof(context)); 39 | } 40 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 41 | context.EnableConcurrentExecution(); 42 | context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); 43 | } 44 | 45 | /// 46 | /// Find OptionAttribute from a property. 47 | /// 48 | /// 49 | private void AnalyzeProperty(SyntaxNodeAnalysisContext context) 50 | { 51 | var propertyNode = (PropertyDeclarationSyntax)context.Node; 52 | 53 | foreach (var attributeSyntax in propertyNode.AttributeLists.SelectMany(x => x.Attributes)) 54 | { 55 | string? attributeName = attributeSyntax.Name switch 56 | { 57 | IdentifierNameSyntax identifierName => identifierName.ToString(), 58 | QualifiedNameSyntax qualifiedName => qualifiedName.ChildNodes().OfType().LastOrDefault()?.ToString(), 59 | _ => null, 60 | }; 61 | 62 | if (attributeName != null && _attributeNames.Contains(attributeName)) 63 | { 64 | var (name, location) = AnalyzeOptionAttributeArguments(attributeSyntax); 65 | if (name != null && location != null) 66 | { 67 | var diagnostic = Diagnostic.Create(Diagnostics.DCL101_OptionLongNameMustBeKebabCase, location, name); 68 | context.ReportDiagnostic(diagnostic); 69 | } 70 | break; 71 | } 72 | } 73 | } 74 | 75 | /// 76 | /// Find LongName argument from the OptionAttribute. 77 | /// 78 | /// 79 | /// 80 | /// name: the LongName value. 81 | /// location: the syntax tree location of the LongName argument value. 82 | /// 83 | private (string? name, Location? location) AnalyzeOptionAttributeArguments(AttributeSyntax attributeSyntax) 84 | { 85 | var argumentList = attributeSyntax.ChildNodes().OfType().FirstOrDefault(); 86 | if (argumentList != null) 87 | { 88 | var attributeArguments = argumentList.ChildNodes().OfType().ToList(); 89 | var longNameExpression = attributeArguments.FirstOrDefault()?.Expression as LiteralExpressionSyntax; 90 | var longName = longNameExpression?.Token.ValueText; 91 | var ignoreCaseExpression = 92 | attributeArguments.FirstOrDefault(x => x.NameEquals?.Name.ToString() == "ExactSpelling")?.Expression as LiteralExpressionSyntax; 93 | var exactSpelling = ignoreCaseExpression?.Token.ValueText.Equals("true", StringComparison.OrdinalIgnoreCase) is true; 94 | if (!exactSpelling && longName is not null) 95 | { 96 | var kebabCase = NamingHelper.MakeKebabCase(longName, true, false); 97 | var isKebabCase = string.Equals(kebabCase, longName, StringComparison.Ordinal); 98 | if (!isKebabCase) 99 | { 100 | return (longName, longNameExpression?.GetLocation()); 101 | } 102 | } 103 | } 104 | return (null, null); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using DotNetCampus.CommandLine.Properties; 9 | using DotNetCampus.Cli.Utils; 10 | 11 | namespace DotNetCampus.CommandLine.Analyzers; 12 | 13 | /// 14 | /// [Option("LongName")] 15 | /// The LongName must be kebab-case. If not, this codefix will fix it. 16 | /// 17 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionLongNameMustBeKebabCaseCodeFixProvider)), Shared] 18 | public class OptionLongNameMustBeKebabCaseCodeFixProvider : CodeFixProvider 19 | { 20 | public sealed override ImmutableArray FixableDiagnosticIds => [Diagnostics.OptionLongNameMustBeKebabCase]; 21 | 22 | public sealed override FixAllProvider GetFixAllProvider() 23 | { 24 | // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers 25 | return WellKnownFixAllProviders.BatchFixer; 26 | } 27 | 28 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 29 | { 30 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 31 | if (root is null) 32 | { 33 | return; 34 | } 35 | 36 | var diagnostic = context.Diagnostics.First(); 37 | var diagnosticSpan = diagnostic.Location.SourceSpan; 38 | 39 | ExpressionSyntax? syntax = root.FindNode(diagnosticSpan) switch 40 | { 41 | AttributeArgumentSyntax attributeArgumentSyntax => attributeArgumentSyntax.Expression, 42 | ExpressionSyntax expressionSyntax => expressionSyntax, 43 | _ => null, 44 | }; 45 | 46 | if (syntax != null) 47 | { 48 | context.RegisterCodeFix( 49 | CodeAction.Create( 50 | title: Localizations.DCL101_Fix1, 51 | createChangedSolution: c => MakeKebabCaseAsync(context.Document, syntax, c), 52 | equivalenceKey: Localizations.DCL101_Fix1), 53 | diagnostic); 54 | } 55 | } 56 | 57 | private async Task MakeKebabCaseAsync(Document document, ExpressionSyntax expressionSyntax, CancellationToken cancellationToken) 58 | { 59 | var expression = expressionSyntax.ToString(); 60 | // 去掉引号。 61 | var oldName = expression.Substring(1, expression.Length - 2); 62 | var newName = NamingHelper.MakeKebabCase(oldName); 63 | 64 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 65 | if (root is null) 66 | { 67 | return document.Project.Solution; 68 | } 69 | 70 | var newRoot = root.ReplaceNode(expressionSyntax, 71 | SyntaxFactory.LiteralExpression( 72 | SyntaxKind.StringLiteralExpression, 73 | SyntaxFactory.Literal(newName))); 74 | return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, newRoot); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Diagnostics.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.CommandLine.Properties; 2 | using Microsoft.CodeAnalysis; 3 | using static DotNetCampus.CommandLine.Properties.Localizations; 4 | 5 | // ReSharper disable InconsistentNaming 6 | 7 | namespace DotNetCampus.CommandLine; 8 | 9 | public static class Diagnostics 10 | { 11 | #region Verb/Value/Options Definition 101-199 12 | 13 | public static readonly DiagnosticDescriptor DCL101_OptionLongNameMustBeKebabCase = new DiagnosticDescriptor( 14 | nameof(DCL101), 15 | Localize(nameof(DCL101)), 16 | Localize(nameof(DCL101_Message)), 17 | Categories.AvoidBugs, 18 | DiagnosticSeverity.Warning, 19 | isEnabledByDefault: true, 20 | description: Localize(nameof(DCL101_Description)), 21 | helpLinkUri: Url(OptionLongNameMustBeKebabCase)); 22 | 23 | #endregion 24 | 25 | #region Options Properties 201-299 26 | 27 | public static readonly DiagnosticDescriptor DCL201_SupportedOptionPropertyType = new DiagnosticDescriptor( 28 | nameof(DCL201), 29 | Localize(nameof(DCL201)), 30 | Localize(nameof(DCL201_Message)), 31 | Categories.CodeFixOnly, 32 | DiagnosticSeverity.Hidden, 33 | isEnabledByDefault: true, 34 | description: Localize(nameof(DCL201_Description)), 35 | helpLinkUri: Url(SupportedOptionPropertyType)); 36 | 37 | public static readonly DiagnosticDescriptor DCL202_NotSupportedOptionPropertyType = new DiagnosticDescriptor( 38 | nameof(DCL202), 39 | Localize(nameof(DCL202)), 40 | Localize(nameof(DCL202_Message)), 41 | Categories.RuntimeException, 42 | DiagnosticSeverity.Error, 43 | isEnabledByDefault: true, 44 | description: Localize(nameof(DCL202_Description)), 45 | helpLinkUri: Url(NotSupportedOptionPropertyType)); 46 | 47 | #endregion 48 | 49 | public const string OptionLongNameMustBeKebabCase = "DCL101"; 50 | public const string SupportedOptionPropertyType = "DCL201"; 51 | public const string NotSupportedOptionPropertyType = "DCL202"; 52 | 53 | private static class Categories 54 | { 55 | /// 56 | /// 可能产生 bug,则报告此诊断。 57 | /// 58 | public const string AvoidBugs = "DotNetCampus.AvoidBugs"; 59 | 60 | /// 61 | /// 为了提供代码生成能力,则报告此诊断。 62 | /// 63 | public const string CodeFixOnly = "DotNetCampus.CodeFixOnly"; 64 | 65 | /// 66 | /// 因编译要求而必须满足的条件没有满足,则报告此诊断。 67 | /// 68 | public const string Compiler = "DotNetCampus.Compiler"; 69 | 70 | /// 71 | /// 因库内的机制限制,必须满足此要求后库才可正常工作,则报告此诊断。 72 | /// 73 | public const string Mechanism = "DotNetCampus.Mechanism"; 74 | 75 | /// 76 | /// 为了代码可读性,使之更易于理解、方便调试,则报告此诊断。 77 | /// 78 | public const string Readable = "DotNetCampus.Readable"; 79 | 80 | /// 81 | /// 为了提升性能,或避免性能问题,则报告此诊断。 82 | /// 83 | public const string Performance = "DotNetCampus.Performance"; 84 | 85 | /// 86 | /// 能写得出来正常编译,但会引发运行时异常,则报告此诊断。 87 | /// 88 | public const string RuntimeException = "DotNetCampus.RuntimeException"; 89 | 90 | /// 91 | /// 编写了无法生效的代码,则报告此诊断。 92 | /// 93 | public const string Useless = "DotNetCampus.Useless"; 94 | } 95 | 96 | private static LocalizableString Localize(string key) => new LocalizableResourceString(key, ResourceManager, typeof(Localizations)); 97 | 98 | public static string Url(string diagnosticId) => $"https://github.com/dotnet-campus/DotNetCampus.CommandLine/docs/analyzers/{diagnosticId}.md"; 99 | } 100 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/DotNetCampus.CommandLine.Analyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | enable 6 | DotNetCampus.CommandLine 7 | true 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | True 27 | Localizations.resx 28 | 29 | 30 | PublicResXFileCodeGenerator 31 | Localizations.Designer.cs 32 | 33 | 34 | Localizations.resx 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/GeneratorInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace DotNetCampus.CommandLine; 5 | 6 | internal static class GeneratorInfo 7 | { 8 | public static string RootNamespace => typeof(GeneratorInfo).Namespace!; 9 | 10 | public static string ToolName { get; } = typeof(GeneratorInfo).Assembly 11 | .GetCustomAttribute()?.Title ?? typeof(GeneratorInfo).Namespace!; 12 | 13 | public static string ToolVersion { get; } = typeof(GeneratorInfo).Assembly 14 | .GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; 15 | 16 | private static readonly SymbolDisplayFormat GlobalDisplayFormat = new SymbolDisplayFormat( 17 | globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, 18 | typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, 19 | genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, 20 | miscellaneousOptions: 21 | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | 22 | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | 23 | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); 24 | 25 | private static readonly SymbolDisplayFormat NotNullGlobalDisplayFormat = new SymbolDisplayFormat( 26 | globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, 27 | typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, 28 | genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, 29 | miscellaneousOptions: 30 | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | 31 | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); 32 | 33 | private static readonly SymbolDisplayFormat GlobalTypeOfDisplayFormat = new SymbolDisplayFormat( 34 | globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, 35 | typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, 36 | genericsOptions: SymbolDisplayGenericsOptions.None, 37 | miscellaneousOptions: 38 | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | 39 | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | 40 | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); 41 | 42 | public static string ToGlobalDisplayString(this ISymbol symbol) 43 | { 44 | return symbol.ToDisplayString(GlobalDisplayFormat); 45 | } 46 | 47 | public static string ToNotNullGlobalDisplayString(this ISymbol symbol) 48 | { 49 | // 对于 Nullable(例如 Nullable、int?)等,是类型而不是可空标记,所以需要特别取出里面的类型 T。 50 | if (symbol is ITypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } typeSymbol) 51 | { 52 | return typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } namedType 53 | // 获取 Nullable 中的 T。 54 | ? namedType.TypeArguments[0].ToDisplayString(GlobalDisplayFormat) 55 | // 处理直接带有可空标记的类型 (int? 这种形式)。 56 | : typeSymbol.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(GlobalDisplayFormat); 57 | } 58 | 59 | // 对于其他符号或非可空类型,使用不包含可空引用类型修饰符的格式 60 | return symbol.ToDisplayString(NotNullGlobalDisplayFormat); 61 | } 62 | 63 | public static string ToGlobalTypeOfDisplayString(this INamedTypeSymbol symbol) 64 | { 65 | var name = symbol.ToDisplayString(GlobalTypeOfDisplayFormat); 66 | return symbol.IsGenericType ? $"{name}<{new string(',', symbol.TypeArguments.Length - 1)}>" : name; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable RSEXPERIMENTAL002 2 | using System.Text.RegularExpressions; 3 | using DotNetCampus.Cli.Compiler; 4 | using DotNetCampus.CommandLine.Utils.CodeAnalysis; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace DotNetCampus.CommandLine.Generators.ModelProviding; 10 | 11 | internal static class InterceptorModelProvider 12 | { 13 | public static IncrementalValuesProvider SelectCommandLineAsProvider(this IncrementalGeneratorInitializationContext context) 14 | { 15 | return SelectMethodInvocationProvider(context, 16 | "DotNetCampus.Cli.CommandLine", "As"); 17 | } 18 | 19 | public static IncrementalValuesProvider SelectCommandBuilderAddHandlerProvider( 20 | this IncrementalGeneratorInitializationContext context) 21 | { 22 | return SelectMethodInvocationProvider(context, "DotNetCampus.Cli.CommandRunnerBuilderExtensions", "AddHandler"); 23 | } 24 | 25 | public static IncrementalValuesProvider SelectCommandBuilderAddHandlerProvider( 26 | this IncrementalGeneratorInitializationContext context, string extensionMethodThisTypeName, string parameterTypeFullName) 27 | { 28 | return SelectMethodInvocationProvider(context, 29 | $"DotNetCampus.Cli.{extensionMethodThisTypeName}", "AddHandler", 30 | parameterTypeFullName.Replace(".", @"\.").Replace("", @"<[\w_\.:\?]+>").Replace(" SelectMethodInvocationProvider(this IncrementalGeneratorInitializationContext context, 34 | string typeFullName, string methodName, params string[] parameterTypeFullNameRegexes) 35 | { 36 | return context.SyntaxProvider.CreateSyntaxProvider((node, ct) => 37 | { 38 | // 检查 commandLine.As() 方法调用。 39 | if (node is InvocationExpressionSyntax 40 | { 41 | Expression: MemberAccessExpressionSyntax 42 | { 43 | Name: GenericNameSyntax 44 | { 45 | TypeArgumentList.Arguments.Count: 1, 46 | } syntax, 47 | }, 48 | } invocationExpressionNode && syntax.Identifier.Text == methodName) 49 | { 50 | // 再检查方法的参数列表是否是指定类型。 51 | var expectedParameterCount = parameterTypeFullNameRegexes.Length; 52 | var argumentList = invocationExpressionNode.ArgumentList.Arguments; 53 | if (argumentList.Count != expectedParameterCount) 54 | { 55 | return false; 56 | } 57 | return true; 58 | } 59 | return false; 60 | }, (c, ct) => 61 | { 62 | var node = (InvocationExpressionSyntax)c.Node; 63 | // 确保此方法是 DotNetCampus.Cli.CommandLine.As() 方法(类也要匹配)。 64 | var methodSymbol = ModelExtensions.GetSymbolInfo(c.SemanticModel, node, ct).Symbol as IMethodSymbol; 65 | if (methodSymbol is null) 66 | { 67 | // 没有方法。 68 | return null; 69 | } 70 | if (methodSymbol.ContainingType.ToDisplayString() != typeFullName 71 | && methodSymbol.ReceiverType?.ToDisplayString() != typeFullName) 72 | { 73 | // 方法所在的类型不匹配,且扩展方法的 this 参数类型不匹配。 74 | return null; 75 | } 76 | if (methodSymbol.Parameters.Length != parameterTypeFullNameRegexes.Length) 77 | { 78 | // 参数数量不匹配。 79 | return null; 80 | } 81 | for (var i = 0; i < parameterTypeFullNameRegexes.Length; i++) 82 | { 83 | var parameterSymbol = methodSymbol.Parameters[i]; 84 | if (!Regex.IsMatch(parameterSymbol.Type.ToGlobalDisplayString(), parameterTypeFullNameRegexes[i])) 85 | { 86 | // 参数类型不匹配。 87 | return null; 88 | } 89 | } 90 | 91 | // 获取 commandLine.As() 中的 T。 92 | var genericTypeNode = ((GenericNameSyntax)((MemberAccessExpressionSyntax)node.Expression).Name).TypeArgumentList.Arguments[0]; 93 | var symbol = ModelExtensions.GetSymbolInfo(c.SemanticModel, genericTypeNode, ct).Symbol as INamedTypeSymbol; 94 | var interceptableLocation = c.SemanticModel.GetInterceptableLocation(node, ct); 95 | if (interceptableLocation is null || symbol is null) 96 | { 97 | return null; 98 | } 99 | // 获取 [Verb("xxx")] 特性中的 xxx。 100 | var verbName = symbol.GetAttributes() 101 | .FirstOrDefault(attribute => attribute.AttributeClass?.IsAttributeOf() is true)? 102 | .ConstructorArguments.FirstOrDefault() is { Kind: TypedConstantKind.Primitive } verbArgument 103 | ? verbArgument.Value?.ToString() 104 | : null; 105 | // 获取调用代码所在的类和方法。 106 | var methodDeclaration = node.FirstAncestorOrSelf(); 107 | var classDeclaration = methodDeclaration?.FirstAncestorOrSelf(); 108 | var invocationFileName = Path.GetFileName(node.SyntaxTree.FilePath); 109 | var invocationInfo = $"{classDeclaration?.Identifier.ToString()}.{methodDeclaration?.Identifier.ToString()} @{invocationFileName}"; 110 | 111 | return new InterceptorGeneratingModel(interceptableLocation, symbol, verbName, invocationInfo); 112 | }) 113 | .Where(model => model is not null) 114 | .Select((model, ct) => model!); 115 | } 116 | } 117 | 118 | internal record InterceptorGeneratingModel( 119 | InterceptableLocation InterceptableLocation, 120 | INamedTypeSymbol CommandObjectType, 121 | string? VerbName, 122 | string InvocationInfo 123 | ) 124 | { 125 | public string GetBuilderTypeName() => CommandObjectGeneratingModel.GetBuilderTypeName(CommandObjectType); 126 | 127 | internal static IEqualityComparer CommandObjectTypeEqualityComparer { get; } = 128 | new PrivateTypeSymbolEqualityComparer(); 129 | 130 | private sealed class PrivateTypeSymbolEqualityComparer : IEqualityComparer 131 | { 132 | public bool Equals(InterceptorGeneratingModel? x, InterceptorGeneratingModel? y) 133 | { 134 | if (ReferenceEquals(x, y)) return true; 135 | if (x is null) return false; 136 | if (y is null) return false; 137 | if (x.GetType() != y.GetType()) return false; 138 | return SymbolEqualityComparer.Default.Equals(x.CommandObjectType, y.CommandObjectType); 139 | } 140 | 141 | public int GetHashCode(InterceptorGeneratingModel obj) 142 | { 143 | return SymbolEqualityComparer.Default.GetHashCode(obj.CommandObjectType); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | text/microsoft-resx 111 | 112 | 113 | 2.0 114 | 115 | 116 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, 117 | PublicKeyToken=b77a5c561934e089 118 | 119 | 120 | 121 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, 122 | PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | 126 | Use 'bool' type instead 127 | 128 | 129 | Use `IReadOnlyDictionary<string, string>` type instead 130 | 131 | 132 | Use 'double' type instead 133 | 134 | 135 | Use 'int' type instead 136 | 137 | 138 | Use `IReadOnlyList<string>` type instead 139 | 140 | 141 | Use 'string' type instead 142 | 143 | 144 | This property has the type '{0}' which is not built-in supported. It's recommended to use bool/string/IReadOnlyList<string> or other types that the code fix will suggest you change instead or add a custom converter on your Value or Option attribute. 145 | 146 | 147 | This property has the type '{0}' which is not built-in supported. 148 | 149 | 150 | Not supported property type 151 | 152 | 153 | The command-line option definition names should be kebab-case, even though you can use any kind of style in the command line environment. 154 | 155 | 156 | Convert to kebab-case 157 | 158 | 159 | The option definition long name '{0}' should be kebab-case, even though you can use any kind of style in the command line environment. 160 | 161 | 162 | Option long name should be kebab-case 163 | 164 | 165 | This property has a recommended option property type '{0}'. 166 | 167 | 168 | This property has a recommended option property type '{0}'. 169 | 170 | 171 | Recommended option property type 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans-cn.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | text/microsoft-resx 4 | 5 | 6 | 1.3 7 | 8 | 9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, 10 | PublicKeyToken=b77a5c561934e089 11 | 12 | 13 | 14 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, 15 | PublicKeyToken=b77a5c561934e089 16 | 17 | 18 | 19 | 命令行选项:改为布尔类型 (bool) 20 | 21 | 22 | 命令行选项:改为字典类型 (IReadOnlyDictionary<string, string>) 23 | 24 | 25 | 命令行选项:改为浮点数类型 (double) 26 | 27 | 28 | 命令行选项:改为整数类型 (int) 29 | 30 | 31 | 命令行选项:改为列表类型 (IReadOnlyList<string>) 32 | 33 | 34 | 命令行选项:改为字符串类型 (string) 35 | 36 | 37 | 作为命令行选项,此属性的类型 {0} 不被内置支持。 建议使用 bool / string / IReadOnlyList<string> 或代码修复会建议您更改的其他类型,或者在 Value / Option 特性上添加自定义转换器。 38 | 39 | 40 | 作为命令行选项,此属性的类型 {0} 不被内置支持。 41 | 42 | 43 | 不支持此类型的命令行属性 44 | 45 | 46 | 选项名称 {0} 应该使用 kebab-case 命名法命名,即使真实使用命令行传参的时候你可以使用任意风格,但定义应该是 kebab-case 风格。 47 | 48 | 49 | 选项名称的定义建议使用 kebab-case 命名法命名,即使真实使用命令行传参的时候你可以使用任意风格,但定义也应该是 kebab-case 风格。 50 | 51 | 52 | 改成 kebab-case 命名法 53 | 54 | 55 | 选项名称建议使用 kebab-case 命名法 56 | 57 | 58 | 此属性的类型 {0} 符合命令行选项的类型要求。 59 | 60 | 61 | 此属性的类型 {0} 符合命令行选项的类型要求。 62 | 63 | 64 | 符合要求的命令行选项类型 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "DotNetCampus.CommandLine.Performance": { 5 | "commandName": "DebugRoslynComponent", 6 | "targetProject": "../../tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj" 7 | }, 8 | "DotNetCampus.CommandLine.Sample": { 9 | "commandName": "DebugRoslynComponent", 10 | "targetProject": "../../samples/DotNetCampus.CommandLine.Sample/DotNetCampus.CommandLine.Sample.csproj" 11 | }, 12 | "DotNetCampus.CommandLine.Tests": { 13 | "commandName": "DebugRoslynComponent", 14 | "targetProject": "../../tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Utils/CodeAnalysis/AnalyzerConfigOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | 4 | namespace DotNetCampus.CommandLine.Utils.CodeAnalysis; 5 | 6 | internal static class AnalyzerConfigOptionsExtensions 7 | { 8 | public static bool GetBoolean(this AnalyzerConfigOptions options, string key) 9 | { 10 | return options.TryGetValue($"build_property.{key}", out var v) 11 | && (v?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false); 12 | } 13 | 14 | public static AnalyzerConfigOptionResult TryGetValue( 15 | this AnalyzerConfigOptions options, 16 | string key, 17 | out T value) 18 | where T : notnull 19 | { 20 | if (options.TryGetValue($"build_property.{key}", out var stringValue)) 21 | { 22 | value = ConvertFromString(stringValue); 23 | return new AnalyzerConfigOptionResult(options, true) 24 | { 25 | UnsetPropertyNames = [], 26 | }; 27 | } 28 | 29 | value = default!; 30 | return new AnalyzerConfigOptionResult(options, false) 31 | { 32 | UnsetPropertyNames = [key], 33 | }; 34 | } 35 | 36 | public static AnalyzerConfigOptionResult TryGetValue( 37 | this AnalyzerConfigOptionResult builder, 38 | string key, 39 | out T value) 40 | where T : notnull 41 | { 42 | var options = builder.Options; 43 | 44 | if (options.TryGetValue($"build_property.{key}", out var stringValue)) 45 | { 46 | value = ConvertFromString(stringValue); 47 | return builder.Link(true, key); 48 | } 49 | 50 | value = default!; 51 | return builder.Link(false, key); 52 | } 53 | 54 | private static T ConvertFromString(string value) 55 | { 56 | if (typeof(T) == typeof(string)) 57 | { 58 | return (T)(object)value; 59 | } 60 | if (typeof(T) == typeof(bool)) 61 | { 62 | return (T)(object)value.Equals("true", StringComparison.OrdinalIgnoreCase); 63 | } 64 | return default!; 65 | } 66 | } 67 | 68 | public readonly record struct AnalyzerConfigOptionResult(AnalyzerConfigOptions Options, bool GotValue) 69 | { 70 | public required ImmutableList UnsetPropertyNames { get; init; } 71 | 72 | public AnalyzerConfigOptionResult Link(bool result, string propertyName) 73 | { 74 | if (result) 75 | { 76 | return this; 77 | } 78 | 79 | if (propertyName is null) 80 | { 81 | throw new ArgumentNullException(nameof(propertyName), @"The property name must be specified if the result is false."); 82 | } 83 | 84 | return this with 85 | { 86 | GotValue = false, 87 | UnsetPropertyNames = UnsetPropertyNames.Add(propertyName), 88 | }; 89 | } 90 | 91 | public static implicit operator bool(AnalyzerConfigOptionResult result) => result.GotValue; 92 | } 93 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Utils/CodeAnalysis/AttributeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace DotNetCampus.CommandLine.Utils.CodeAnalysis; 5 | 6 | public static class AttributeExtensions 7 | { 8 | public static bool IsAttributeOf(this AttributeSyntax attribute) 9 | { 10 | var codeName = attribute.Name.ToString(); 11 | var compareName = typeof(TAttribute).Name; 12 | if (codeName == compareName) 13 | { 14 | return true; 15 | } 16 | 17 | if (compareName.EndsWith("Attribute")) 18 | { 19 | compareName = compareName.Substring(0, compareName.Length - "Attribute".Length); 20 | if (codeName == compareName) 21 | { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public static bool IsAttributeOf(this INamedTypeSymbol attribute) 30 | { 31 | if (attribute.ContainingNamespace.ToString() != typeof(TAttribute).Namespace) 32 | { 33 | return false; 34 | } 35 | 36 | var compareName = typeof(TAttribute).Name; 37 | if (attribute.Name == typeof(TAttribute).Name) 38 | { 39 | return true; 40 | } 41 | 42 | if (compareName.EndsWith("Attribute")) 43 | { 44 | compareName = compareName.Substring(0, compareName.Length - "Attribute".Length); 45 | if (attribute.Name == compareName) 46 | { 47 | return true; 48 | } 49 | } 50 | 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine.Analyzer/Utils/CodeAnalysis/TypeSymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace DotNetCampus.CommandLine.Utils.CodeAnalysis; 4 | 5 | /// 6 | /// 为 提供扩展方法。 7 | /// 8 | internal static class TypeSymbolExtensions 9 | { 10 | /// 11 | /// 判断一个类型是否是指定类型的子类或实现了指定接口。 12 | /// 13 | /// 要判断的类型。 14 | /// 基类或接口的完整名称(对于泛型类型,不包含泛型参数)。 15 | /// 当类型与指定类型完全匹配时是否返回 。 16 | /// 如果是指定类型的子类或实现了指定接口,则返回 ;否则返回 17 | public static bool IsSubclassOrImplementOf(this ITypeSymbol type, IReadOnlyCollection baseTypeOrInterfaceNames, bool trueIfExactMatch = false) 18 | { 19 | var typeName = $"{type.ContainingNamespace}.{type.Name}"; 20 | var isExactMatch = baseTypeOrInterfaceNames.Contains(typeName); 21 | if (isExactMatch) 22 | { 23 | return trueIfExactMatch; 24 | } 25 | 26 | foreach (var baseTypeOrInterfaceName in baseTypeOrInterfaceNames) 27 | { 28 | var baseType = type.BaseType; 29 | while (baseType is not null) 30 | { 31 | var name = baseType.ToDisplayString(); 32 | if (name == baseTypeOrInterfaceName || name.StartsWith($"{baseTypeOrInterfaceName}<")) 33 | { 34 | return true; 35 | } 36 | 37 | baseType = baseType.BaseType; 38 | } 39 | 40 | foreach (var @interface in type.AllInterfaces) 41 | { 42 | var name = @interface.ToDisplayString(); 43 | if (name == baseTypeOrInterfaceName) 44 | { 45 | return true; 46 | } 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/CommandLineParsingOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace DotNetCampus.Cli; 4 | 5 | /// 6 | /// 在解析命令行参数时,指定命令行参数的解析方式。 7 | /// 8 | public readonly record struct CommandLineParsingOptions() 9 | { 10 | /// 11 | public static CommandLineParsingOptions Flexible => new CommandLineParsingOptions 12 | { 13 | Style = CommandLineStyle.Flexible, 14 | CaseSensitive = false, 15 | }; 16 | 17 | /// 18 | public static CommandLineParsingOptions Gnu => new CommandLineParsingOptions 19 | { 20 | Style = CommandLineStyle.Gnu, 21 | CaseSensitive = true, 22 | }; 23 | 24 | /// 25 | public static CommandLineParsingOptions Posix => new CommandLineParsingOptions 26 | { 27 | Style = CommandLineStyle.Posix, 28 | CaseSensitive = true, 29 | }; 30 | 31 | /// 32 | public static CommandLineParsingOptions DotNet => new CommandLineParsingOptions 33 | { 34 | Style = CommandLineStyle.DotNet, 35 | CaseSensitive = false, 36 | }; 37 | 38 | /// 39 | public static CommandLineParsingOptions PowerShell => new CommandLineParsingOptions 40 | { 41 | Style = CommandLineStyle.PowerShell, 42 | CaseSensitive = false, 43 | }; 44 | 45 | /// 46 | /// 以此风格解析命令行参数。 47 | /// 48 | /// 49 | /// 不指定时会自动根据用户输入的命令行参数判断风格。 50 | /// 51 | public CommandLineStyle Style { get; init; } 52 | 53 | /// 54 | /// 默认是大小写不敏感的,设置此值为 可以让命令行参数大小写敏感。 55 | /// 56 | /// 57 | /// 当然,可以在单独的属性上设置大小写敏感,设置后将在那个属性上覆盖此默认值。不设置的属性会使用此默认值。 58 | /// 59 | public bool CaseSensitive { get; init; } 60 | 61 | /// 62 | /// 此命令行解析器支持从 Web 打开本地应用时传入的参数。
63 | /// 此属性指定用于 URI 协议注册的方案名(scheme name)。 64 | ///
65 | /// 66 | /// 67 | /// 例如:sample://open?url=DotNetCampus%20is%20a%20great%20team
68 | /// 这里的 "sample" 就是方案名。
69 | /// 当解析命令行参数时,如果只传入了一个参数,且参数开头满足 sample:// 格式时,则会认为方案名匹配,将进行后续 url 的参数解析。设置此属性后,无论选择哪种命令行风格(),都会优先识别并解析URL格式的参数。 70 | ///
71 | /// /// 72 | /// URL风格命令行参数模拟Web请求中的查询字符串格式,适用于习惯于Web开发的用户,以及需要通过URL协议方案(URL Scheme)启动的应用程序。
73 | ///
74 | /// 详细规则:
75 | /// 1. 完整格式为 [scheme://][path][?option1=value1&option2=value2]
76 | /// 2. 参数部分以问号(?)开始,后面是键值对
77 | /// 3. 多个参数之间用(&)符号分隔
78 | /// 4. 每个参数的键值之间用等号(=)分隔
79 | /// 5. 支持URL编码规则,如空格编码为%20,特殊字符需编码
80 | /// 6. 支持数组格式参数,如tags=tag1&tags=tag2表示tags参数有多个值
81 | /// 7. 支持无值参数,被视为布尔值true,如?enabled
82 | /// 8. 参数值为空字符串时保留等号,如?name=
83 | /// 9. 路径部分(path)一般情况下会被视为位置参数,例如 myapp://documents/open 中,documents/open 被视为位置参数
84 | /// 10. 但在某些情况下,路径的第一个部分可能会被当作谓词(命令),例如 myapp://open/file.txt 中,open 可能是谓词,file.txt 是位置参数。具体解释为位置参数还是谓词取决于应用的命令行处理器实现
85 | /// 11. 整个URL可以用引号包围,以避免特殊字符被shell解释
86 | ///
87 | /// 88 | /// # 完整URL格式(通常由Web浏览器或其他应用程序传递) 89 | /// myapp://open?url=https://example.com # 包含方案(scheme)、路径和参数 90 | /// myapp://user/profile?id=123&tab=info # 带层级路径 91 | /// sample://document/edit?id=42&mode=full # 多参数和路径组合 92 | /// 93 | /// # 特殊字符与编码 94 | /// yourapp://search?q=hello%20world # 编码空格 95 | /// myapp://open?query=C%23%20programming # 特殊字符编码 96 | /// appname://tags?value=c%23&value=.net # 数组参数(相同参数名多次出现) 97 | /// 98 | /// # 无值和空值参数 99 | /// myapp://settings?debug # 无值参数(视为true) 100 | /// yourapp://profile?name=&id=123 # 空字符串值 101 | /// 102 | /// # 路径与谓词示例 103 | /// myapp://documents/open?readonly=true # documents 和 open 作为位置参数 104 | /// myapp://open/file.txt?temporary=true # open 是谓词,file.txt 是位置参数;或 open 和 file.txt 都是位置参数 105 | /// 106 | ///
107 | public ImmutableArray SchemeNames { get; init; } = []; 108 | } 109 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/CommandRunner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.ComponentModel; 3 | using DotNetCampus.Cli.Compiler; 4 | using DotNetCampus.Cli.Exceptions; 5 | using DotNetCampus.Cli.Utils.Handlers; 6 | 7 | namespace DotNetCampus.Cli; 8 | 9 | /// 10 | /// 辅助 根据已解析的命令行参数执行对应的命令处理器。 11 | /// 12 | public class CommandRunner : ICommandRunnerBuilder, IAsyncCommandRunnerBuilder 13 | { 14 | private static ConcurrentDictionary CommandObjectCreationInfos { get; } = new(ReferenceEqualityComparer.Instance); 15 | 16 | private readonly CommandLine _commandLine; 17 | private readonly DictionaryCommandHandlerCollection _dictionaryVerbHandlers = new(); 18 | private readonly ConcurrentDictionary _assemblyVerbHandlers = []; 19 | 20 | internal CommandRunner(CommandLine commandLine) 21 | { 22 | _commandLine = commandLine; 23 | } 24 | 25 | internal CommandRunner(CommandRunner commandRunner) 26 | { 27 | _commandLine = commandRunner._commandLine; 28 | } 29 | 30 | /// 31 | /// 供源生成器调用,注册一个专门用来处理谓词 的命令处理器。 32 | /// 33 | /// 关联的谓词。 34 | /// 命令处理器的创建方法。 35 | /// 选项类型,或命令处理器类型,或任意类型。 36 | [EditorBrowsable(EditorBrowsableState.Never)] 37 | public static void Register(string? verbName, CommandObjectCreator creator) 38 | where T : class 39 | { 40 | CommandObjectCreationInfos[typeof(T)] = new CommandObjectCreationInfo(verbName, creator); 41 | } 42 | 43 | /// 44 | /// 创建一个命令处理器实例。 45 | /// 46 | /// 已解析的命令行参数。 47 | /// 命令处理器的类型。 48 | /// 命令处理器实例。 49 | internal static T CreateInstance(CommandLine commandLine) 50 | { 51 | if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) 52 | { 53 | throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); 54 | } 55 | 56 | return (T)info.Creator(commandLine); 57 | } 58 | 59 | /// 60 | /// 创建一个命令处理器实例。 61 | /// 62 | /// 已解析的命令行参数。 63 | /// 命令处理器的创建方法。 64 | /// 命令处理器的类型。 65 | /// 命令处理器实例。 66 | internal static T CreateInstance(CommandLine commandLine, CommandObjectCreator creator) 67 | { 68 | return (T)creator(commandLine); 69 | } 70 | 71 | CommandRunner ICoreCommandRunnerBuilder.GetOrCreateRunner() => this; 72 | 73 | /// 74 | /// 添加一个命令处理器。 75 | /// 76 | /// 命令处理器的类型。 77 | /// 返回一个命令处理器构建器。 78 | internal CommandRunner AddHandler() 79 | where T : class, ICommandHandler 80 | { 81 | if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) 82 | { 83 | throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); 84 | } 85 | 86 | _dictionaryVerbHandlers.AddHandler(info.VerbName, cl => (T)info.Creator(cl)); 87 | return this; 88 | } 89 | 90 | /// 91 | /// 添加一个命令处理器。 92 | /// 93 | /// 由拦截器传入的的命令处理器的谓词。 94 | /// 由拦截器传入的命令处理器创建方法。 95 | /// 命令处理器的类型。 96 | /// 返回一个命令处理器构建器。 97 | [EditorBrowsable(EditorBrowsableState.Never)] 98 | internal CommandRunner AddHandler(string? verbName, CommandObjectCreator creator) 99 | where T : class, ICommandHandler 100 | { 101 | _dictionaryVerbHandlers.AddHandler(verbName, creator); 102 | return this; 103 | } 104 | 105 | /// 106 | /// 添加一个命令处理器。 107 | /// 108 | /// 用于处理已解析的命令行参数的委托。 109 | /// 命令处理器的类型。 110 | /// 返回一个命令处理器构建器。 111 | internal CommandRunner AddHandler(Func> handler) 112 | where T : class 113 | { 114 | if (!CommandObjectCreationInfos.TryGetValue(typeof(T), out var info)) 115 | { 116 | throw new InvalidOperationException($"Handler '{typeof(T)}' is not registered. This may be a bug of the source generator."); 117 | } 118 | 119 | _dictionaryVerbHandlers.AddHandler(info.VerbName, cl => new TaskCommandHandler( 120 | () => (T)info.Creator(cl), 121 | handler)); 122 | return this; 123 | } 124 | 125 | /// 126 | /// 添加一个命令处理器。 127 | /// 128 | /// 由拦截器传入的的命令处理器的谓词。 129 | /// 由拦截器传入的命令处理器创建方法。 130 | /// 用于处理已解析的命令行参数的委托。 131 | /// 命令处理器的类型。 132 | /// 返回一个命令处理器构建器。 133 | internal CommandRunner AddHandler(string? verbName, CommandObjectCreator creator, Func> handler) 134 | where T : class 135 | { 136 | _dictionaryVerbHandlers.AddHandler(verbName, cl => new TaskCommandHandler( 137 | () => (T)creator(cl), 138 | handler)); 139 | return this; 140 | } 141 | 142 | internal CommandRunner AddHandlers() 143 | where T : ICommandHandlerCollection, new() 144 | { 145 | var c = new T(); 146 | _assemblyVerbHandlers.TryAdd(c, c); 147 | return this; 148 | } 149 | 150 | private ICommandHandler? MatchHandler() 151 | { 152 | var verbName = _commandLine.GuessedVerbName; 153 | 154 | // 优先寻找单独添加的处理器。 155 | if (_dictionaryVerbHandlers.TryMatch(verbName, _commandLine) is { } h1) 156 | { 157 | return h1; 158 | } 159 | 160 | // 其次寻找程序集中自动搜集到的处理器。 161 | foreach (var handler in _assemblyVerbHandlers) 162 | { 163 | if (handler.Value.TryMatch(verbName, _commandLine) is { } h2) 164 | { 165 | return h2; 166 | } 167 | } 168 | 169 | // 如果没有找到,那么很可能此命令没有谓词,需要使用默认的处理器。 170 | if (_dictionaryVerbHandlers.TryMatch(null, _commandLine) is { } h3) 171 | { 172 | return h3; 173 | } 174 | foreach (var handler in _assemblyVerbHandlers) 175 | { 176 | if (handler.Value.TryMatch(null, _commandLine) is { } h4) 177 | { 178 | return h4; 179 | } 180 | } 181 | 182 | // 如果连默认的处理器都没有找到,说明根本没有能处理此命令的处理器。 183 | return null; 184 | } 185 | 186 | /// 187 | public int Run() 188 | { 189 | return RunAsync().Result; 190 | } 191 | 192 | /// 193 | public Task RunAsync() 194 | { 195 | var handler = MatchHandler(); 196 | 197 | if (handler is null) 198 | { 199 | throw new CommandVerbNotFoundException( 200 | $"No command handler found for verb '{_commandLine.GuessedVerbName}'. Please ensure that the command handler is registered correctly.", 201 | _commandLine.GuessedVerbName); 202 | } 203 | 204 | return handler.RunAsync(); 205 | } 206 | 207 | private readonly record struct CommandObjectCreationInfo(string? VerbName, CommandObjectCreator Creator); 208 | } 209 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/CollectCommandHandlersFromThisAssemblyAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 在一个 partial 类上标记,源生成器会自动查找此类型所在项目中所有支持的命令,并允许添加到 命令行解析器中执行。 5 | /// 6 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 7 | public class CollectCommandHandlersFromThisAssemblyAttribute : Attribute 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/CommandLineAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace DotNetCampus.Cli.Compiler; 4 | 5 | /// 6 | /// 为命令行参数与类型属性的关联提供特性基类。 7 | /// 8 | public abstract class CommandLineAttribute : Attribute 9 | { 10 | /// 11 | /// 只允许内部的类型继承自 。 12 | /// 13 | internal CommandLineAttribute() 14 | { 15 | } 16 | 17 | /// 18 | /// 此命令行类/属性的描述信息(会在命令行输出帮助信息时使用)。 19 | /// 20 | /// 21 | /// 如需支持本地化,请 // TODO: 添加本地化支持。 22 | /// 23 | [DisallowNull] 24 | public string? Description { get; set; } 25 | 26 | /// 27 | /// 未来不再支持使用本方式的本地化。 28 | /// 29 | [DisallowNull] 30 | [Obsolete("不再使用单一的本地化方法,请使用即将开发的新方法替代。")] 31 | public string? LocalizableDescription { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/CommandObjectCreator.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 从已解析的命令行参数创建命令数据模型或处理器的委托。 5 | /// 6 | public delegate object CommandObjectCreator(CommandLine commandLine); 7 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/ICommandHandlerCollection.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 管理一组命令处理器的集合,在谓词匹配的情况下辅助执行对应的命令处理器。 5 | /// 6 | public interface ICommandHandlerCollection 7 | { 8 | /// 9 | /// 尝试匹配一个命令处理器。 10 | /// 11 | /// 要匹配的谓词。 12 | /// 已解析的命令行参数。 13 | /// 匹配的命令处理器,如果没有匹配的命令处理器,则返回 14 | ICommandHandler? TryMatch(string? verb, CommandLine commandLine); 15 | } 16 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/OptionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 标记一个属性为命令行选项。 5 | /// 6 | /// 7 | /// 示例用法: 8 | /// 9 | /// [Option("property-name")] 10 | /// public required string PropertyName { get; init; } 11 | /// 12 | /// 其中: 13 | /// 14 | /// required 为可选的修饰符,表示该选项为必填项;如果没有在命令行中传入,则会抛出异常或输出错误信息。 15 | /// 选项名称可以不指定,当不指定时将自动使用属性名。 16 | /// 选项名称建议使用 kebab-case 命名法(以获得更好的大小写和数字的区分度;当然,这并不影响实际使用,你仍可以使用其他命令行风格的命名法传入命令行参数)。 17 | /// 18 | /// 如果希望传入多个参数,则可以使用数组类型: 19 | /// 20 | /// [Option("property-name")] 21 | /// public ImmutableArray<string> Values { get; init; } 22 | /// 23 | /// 数组、常用的只读/不可变集合类型(包括接口)都是支持的,例如: 24 | /// 25 | /// string[] 26 | /// IReadOnlyList<string> 27 | /// ImmutableArray<string> 28 | /// ImmutableHashSet<string> 29 | /// IReadOnlyDictionary<string, string> 30 | /// ImmutableDictionary<string, string> 31 | /// 32 | /// 对于字典类型的属性,命令行可通过如下方式传入: 33 | /// 34 | /// do --property-name key1=value1 --property-name key2=value2 35 | /// do --property-name:key1=value1;key2=value2 36 | /// 37 | /// 38 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 39 | public sealed class OptionAttribute : CommandLineAttribute 40 | { 41 | /// 42 | /// 标记一个属性为命令行选项,其长名称为属性名。 43 | /// 44 | public OptionAttribute() 45 | { 46 | } 47 | 48 | /// 49 | /// 标记一个属性为命令行选项,并具有指定的长名称。 50 | /// 51 | /// 选项的短名称。必须是单个字符。 52 | public OptionAttribute(char shortName) 53 | { 54 | if (!char.IsLetter(shortName)) 55 | { 56 | throw new ArgumentException($"选项的短名称必须是字母字符,但实际为 '{shortName}'。", nameof(shortName)); 57 | } 58 | 59 | ShortName = shortName; 60 | } 61 | 62 | /// 63 | /// 标记一个属性为命令行选项,并具有指定的长名称。 64 | /// 65 | /// 66 | /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 67 | /// 68 | public OptionAttribute(string longName) 69 | { 70 | LongName = longName; 71 | } 72 | 73 | /// 74 | /// 标记一个属性为命令行选项,并具有指定的长名称和短名称。 75 | /// 76 | /// 选项的短名称。必须是单个字符。 77 | /// 78 | /// 选项名称。必须使用 kebab-case 命名规则,且不带 -- 前缀。 79 | /// 80 | public OptionAttribute(char shortName, string longName) 81 | { 82 | if (!char.IsLetter(shortName)) 83 | { 84 | throw new ArgumentException($"选项的短名称必须是字母字符,但实际为 '{shortName}'。", nameof(shortName)); 85 | } 86 | 87 | LongName = longName; 88 | ShortName = shortName; 89 | } 90 | 91 | /// 92 | /// 获取或初始化选项的短名称。 93 | /// 94 | public char ShortName { get; } = '\0'; 95 | 96 | /// 97 | /// 获取选项的长名称。 98 | /// 99 | public string? LongName { get; } 100 | 101 | /// 102 | /// 获取或设置选项的别名。 103 | /// 104 | /// 105 | /// 可以指定短名称(如 `v`)或长名称(如 `verbose`)。单个字符的别名会被视为短名称。
106 | /// 如果指定区分大小写,但期望允许部分单词使用多种大小写,则应该在别名中指定多个大小写形式。如将 `verbose` 的别名指定为 `verbose Verbose VERBOSE`。 107 | ///
108 | public string[] Aliases { get; init; } = []; 109 | 110 | /// 111 | /// 获取或设置是否大小写敏感。 112 | /// 113 | /// 114 | /// 默认情况下使用 解析时所指定的大小写敏感性(而 默认为大小写不敏感)。 115 | /// 116 | public bool CaseSensitive { get; init; } 117 | 118 | /// 119 | /// 命令行参数中传入的选项名称必须严格保持与此属性中指定的长名称一致。 120 | /// 121 | /// 122 | /// 默认情况下,我们会为了支持多种不同的命令行风格而自动识别选项的长名称,例如: 123 | /// 124 | /// 属性名 SampleProperty 可匹配:--Sample-Property --sample-property -SampleProperty 125 | /// 属性名 sample-property 可匹配:--Sample-Property --sample-property -SampleProperty 126 | /// 127 | /// 但设置了此属性为 后,命令行中传入的选项名称必须完全一致: 128 | /// 129 | /// 属性名 SampleProperty 可匹配:--SampleProperty --sampleproperty -SampleProperty 130 | /// 属性名 sample-property 可匹配:--Sample-Property --sample-property -Sample-Property 131 | /// 132 | /// 133 | public bool ExactSpelling { get; init; } 134 | } 135 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/ValueAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 标记一个属性为命令行位置参数。 5 | /// 6 | /// 7 | /// 示例用法: 8 | /// 9 | /// [Value] 10 | /// public required string Value { get; init; } 11 | /// 12 | /// 其中: 13 | /// 14 | /// required 为可选的修饰符,表示该选项为必填项;如果没有在命令行中传入,则会抛出异常或输出错误信息。 15 | /// 16 | /// 如果希望传入多个参数,则可以使用数组类型: 17 | /// 18 | /// [Value(Length = int.MaxValue)] 19 | /// public ImmutableArray<string> Values { get; init; } 20 | /// 21 | /// 数组、常用的只读/不可变集合类型(包括接口)都是支持的,例如: 22 | /// 23 | /// string[] 24 | /// IReadOnlyList<string> 25 | /// ImmutableArray<string> 26 | /// ImmutableHashSet<string> 27 | /// IReadOnlyDictionary<string, string> 28 | /// ImmutableDictionary<string, string> 29 | /// 30 | /// 31 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 32 | public sealed class ValueAttribute : CommandLineAttribute 33 | { 34 | /// 35 | /// 获取位置参数的索引。 36 | /// 37 | public int Index { get; } 38 | 39 | /// 40 | /// 获取或初始化从索引处开始的参数个数。 41 | /// 42 | public int Length { get; set; } 43 | 44 | /// 45 | /// 标记一个属性为命令行位置参数,其接收第一个位置参数。 46 | /// 47 | public ValueAttribute() 48 | { 49 | Index = 0; 50 | Length = 1; 51 | } 52 | 53 | /// 54 | /// 标记一个属性为命令行位置参数,其接收指定索引的参数。 55 | /// 56 | /// 指定的位置参数的索引。 57 | public ValueAttribute(int index) 58 | { 59 | Index = index; 60 | Length = 1; 61 | } 62 | 63 | /// 64 | /// 标记一个属性为命令行位置参数,其接收指定索引、指定长度的参数。 65 | /// 66 | /// 指定的位置参数的索引。 67 | /// 指定的位置参数的长度。 68 | public ValueAttribute(int index, int length) 69 | { 70 | Index = index; 71 | Length = length; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Compiler/VerbAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Compiler; 2 | 3 | /// 4 | /// 将一个类绑定一个命令行谓词。 5 | /// 6 | /// 7 | /// 命令行谓词。必须使用 kebab-case 命名规则,且不带 -- 前缀。 8 | /// 可选三种形式: 9 | /// 10 | /// null、空字符串或空白字符串,表示默认命令行谓词。当在启动程序没有传入任何谓词时,会匹配此类型。例如 `dotnet --list-sdks`。 11 | /// 一个 kebab-case 风格的词组,表示一个一级命令行谓词。当启动程序传入的谓词与此相同时,会匹配此类型。例如 `dotnet build`。 12 | /// 多个 kebab-case 风格的词组以“/”分隔,表示一个多级命令行谓词。当启动程序传入多个谓词且逐一匹配时,会匹配此类型。例如 `dotnet sln add`。 13 | /// 14 | /// 15 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] 16 | public sealed class VerbAttribute(string? name) : CommandLineAttribute 17 | { 18 | /// 19 | /// 获取命令行谓词。 20 | /// 21 | public string? Name { get; } = name; 22 | } 23 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net6.0 5 | enable 6 | DotNetCampus.CommandLine 7 | true 8 | DotNetCampus.Cli 9 | true 10 | snupkg 11 | true 12 | true 13 | true 14 | 0 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Exceptions/CommandLineException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Exceptions; 2 | 3 | /// 4 | /// 表示命令行解析或执行过程中发生的异常。 5 | /// 6 | public class CommandLineException : Exception 7 | { 8 | private const string DefaultMessage = "Operation failed due to an error in the command line mechanism."; 9 | 10 | /// 11 | /// 初始化 类的新实例。 12 | /// 13 | public CommandLineException() : base(DefaultMessage) 14 | { 15 | } 16 | 17 | /// 18 | /// 初始化 类的新实例。 19 | /// 20 | /// 异常消息。 21 | public CommandLineException(string message) : base(message) 22 | { 23 | } 24 | 25 | /// 26 | /// 初始化 类的新实例。 27 | /// 28 | /// 异常消息。 29 | /// 内部异常。 30 | public CommandLineException(string message, Exception innerException) : base(message, innerException) 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Exceptions/CommandLineParseException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Exceptions; 2 | 3 | /// 4 | /// 在解析命令行参数的过程中发生的异常。 5 | /// 6 | public class CommandLineParseException : CommandLineException 7 | { 8 | private const string DefaultMessage = "Parse the command line failed."; 9 | 10 | /// 11 | /// 初始化 类的新实例。 12 | /// 13 | public CommandLineParseException() : base(DefaultMessage) 14 | { 15 | } 16 | 17 | /// 18 | /// 初始化 类的新实例。 19 | /// 20 | /// 异常消息。 21 | public CommandLineParseException(string message) : base(message) 22 | { 23 | } 24 | 25 | /// 26 | /// 初始化 类的新实例。 27 | /// 28 | /// 异常消息。 29 | /// 内部异常。 30 | public CommandLineParseException(string message, Exception innerException) : base(message, innerException) 31 | { 32 | } 33 | } 34 | 35 | /// 36 | /// 在解析命令行参数的值的过程中发生的异常。 37 | /// 38 | public class CommandLineParseValueException : CommandLineParseException 39 | { 40 | private const string DefaultMessage = "Failed to parse the command line value."; 41 | 42 | /// 43 | /// 初始化 类的新实例。 44 | /// 45 | public CommandLineParseValueException() : base(DefaultMessage) 46 | { 47 | } 48 | 49 | /// 50 | /// 初始化 类的新实例。 51 | /// 52 | /// 异常消息。 53 | public CommandLineParseValueException(string message) : base(message) 54 | { 55 | } 56 | 57 | /// 58 | /// 初始化 类的新实例。 59 | /// 60 | /// 异常消息。 61 | /// 内部异常。 62 | public CommandLineParseValueException(string message, Exception innerException) : base(message, innerException) 63 | { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Exceptions/CommandVerbAmbiguityException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Exceptions; 2 | 3 | /// 4 | /// 表示多个命令行参数选项、谓词或处理器被标记为匹配同一个命令行谓词的异常。 5 | /// 6 | public class CommandVerbAmbiguityException : CommandLineException 7 | { 8 | /// 9 | /// 获取命令行谓词的名称。 10 | /// 11 | public string? VerbName { get; } 12 | 13 | /// 14 | /// 初始化 类的新实例。 15 | /// 16 | /// 异常提示信息。 17 | /// 命令行谓词的名称。 18 | public CommandVerbAmbiguityException(string message, string? verbName) 19 | : base(message) 20 | { 21 | VerbName = verbName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Exceptions/CommandVerbNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Exceptions; 2 | 3 | /// 4 | /// 表示输入的命令行在匹配多个命令行参数选项、谓词或处理器时,没有任何一个匹配到的异常。 5 | /// 6 | public class CommandVerbNotFoundException : CommandLineException 7 | { 8 | /// 9 | /// 获取命令行谓词的名称。 10 | /// 11 | public string? VerbName { get; } 12 | 13 | /// 14 | /// 初始化 类的新实例。 15 | /// 16 | /// 异常提示信息。 17 | /// 命令行谓词的名称。 18 | public CommandVerbNotFoundException(string message, string? verbName) 19 | : base(message) 20 | { 21 | VerbName = verbName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Exceptions/RequiredPropertyNotAssignedException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Exceptions; 2 | 3 | /// 4 | /// 表示一个必须赋值的属性,没有在命令行参数中赋值的异常。 5 | /// 6 | public class RequiredPropertyNotAssignedException : CommandLineException 7 | { 8 | /// 9 | /// 获取必须属性的名称。 10 | /// 11 | public string? PropertyName { get; } 12 | 13 | /// 14 | /// 初始化 类的新实例。 15 | /// 16 | /// 异常提示信息。 17 | /// 必须属性的名称。 18 | public RequiredPropertyNotAssignedException(string message, string propertyName) 19 | : base(message) 20 | { 21 | PropertyName = propertyName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli; 2 | 3 | /// 4 | /// 表示一个可以接收命令行参数的对象。 5 | /// 6 | public interface ICommandOptions 7 | { 8 | } 9 | 10 | /// 11 | /// 表示可以接收命令行参数,然后处理一条命令。 12 | /// 13 | public interface ICommandHandler : ICommandOptions 14 | { 15 | /// 16 | /// 处理一条命令。 17 | /// 18 | /// 返回处理结果。 19 | Task RunAsync(); 20 | } 21 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli; 2 | 3 | /// 4 | /// 命令行执行器构造器,用于链式创建命令行执行器。 5 | /// 6 | public interface ICoreCommandRunnerBuilder 7 | { 8 | /// 9 | /// 获取或创建一个命令行执行器。 10 | /// 11 | /// 命令行执行器。 12 | internal CommandRunner GetOrCreateRunner(); 13 | } 14 | 15 | /// 16 | /// 命令行执行器构造器,用于链式创建命令行执行器。 17 | /// 18 | public interface ICommandRunnerBuilder : ICoreCommandRunnerBuilder 19 | { 20 | /// 21 | /// 以同步方式运行命令行处理器。 22 | /// 23 | /// 将被执行的命令行处理器的返回值。 24 | int Run(); 25 | } 26 | 27 | /// 28 | /// 命令行执行器构造器,用于链式创建命令行执行器。 29 | /// 30 | public interface IAsyncCommandRunnerBuilder : ICoreCommandRunnerBuilder 31 | { 32 | /// 33 | /// 以异步方式运行命令行处理器。 34 | /// 35 | /// 将被执行的命令行处理器的返回值。 36 | Task RunAsync(); 37 | } 38 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Package/build/Package.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | $(InterceptorsNamespaces);DotNetCampus.CommandLine.Compiler 6 | 7 | 15 | $(InterceptorsPreviewNamespaces);DotNetCampus.CommandLine.Compiler 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Collections/ReadOnlyListRange.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace DotNetCampus.Cli.Utils.Collections; 4 | 5 | /// 6 | /// 从一个只读集合中取出一个范围,让此集合表现得就像那个范围内的一个子集合一样。 7 | /// 8 | /// 集合的元素类型。 9 | internal readonly struct ReadOnlyListRange : IReadOnlyList 10 | { 11 | private readonly IReadOnlyList? _sourceList; 12 | private readonly Range _range; 13 | 14 | /// 15 | /// 从一个只读集合中取出一个范围,让此集合表现得就像那个范围内的一个子集合一样。 16 | /// 17 | /// 原集合。 18 | /// 范围。 19 | public ReadOnlyListRange(IReadOnlyList sourceList, Range range) 20 | { 21 | _sourceList = sourceList; 22 | _range = range; 23 | } 24 | 25 | public int Count => _range.GetOffsetAndLength(_sourceList?.Count ?? 0).Length; 26 | 27 | public T this[int index] => _sourceList is null 28 | ? throw new ArgumentOutOfRangeException(nameof(index)) 29 | : _sourceList[_range.GetOffsetAndLength(_sourceList.Count).Offset + index]; 30 | 31 | public ReadOnlyListRange Slice(int offset, int length) 32 | { 33 | if (_sourceList is null) 34 | { 35 | return offset is 0 && length is 0 36 | ? new ReadOnlyListRange([], new Range(0, 0)) 37 | : throw new ArgumentOutOfRangeException(nameof(length)); 38 | } 39 | 40 | var (start, _) = _range.GetOffsetAndLength(_sourceList.Count); 41 | return new ReadOnlyListRange(_sourceList, new Range(start + offset, start + offset + length)); 42 | } 43 | 44 | public IEnumerator GetEnumerator() 45 | { 46 | if (_sourceList is null) 47 | { 48 | yield break; 49 | } 50 | 51 | var (offset, length) = _range.GetOffsetAndLength(_sourceList.Count); 52 | for (var i = offset; i < offset + length; i++) 53 | { 54 | yield return _sourceList[i]; 55 | } 56 | } 57 | 58 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 59 | } 60 | 61 | internal static class ReadOnlyListRangeExtensions 62 | { 63 | public static ReadOnlyListRange Slice(this IReadOnlyList sourceList, Range range) 64 | { 65 | return new ReadOnlyListRange(sourceList, range); 66 | } 67 | 68 | public static ReadOnlyListRange Slice(this IReadOnlyList sourceList, int offset, int length) 69 | { 70 | return new ReadOnlyListRange(sourceList, new Range(offset, offset + length)); 71 | } 72 | 73 | public static ReadOnlyListRange ToReadOnlyList(this IReadOnlyList sourceList) 74 | { 75 | return new ReadOnlyListRange(sourceList, new Range(0, sourceList.Count)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Collections/SingleOptimizedList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Diagnostics.Contracts; 5 | 6 | namespace DotNetCampus.Cli.Utils.Collections; 7 | 8 | /// 9 | /// 为 0 个和 1 个值特殊优化的列表。 10 | /// 11 | [DebuggerDisplay(nameof(SingleOptimizedList) + " {_firstValue,nq}, {_restValues}")] 12 | internal readonly struct SingleOptimizedList : IReadOnlyList 13 | { 14 | /// 15 | /// 是否有值。如果为 ,则是空列表。 16 | /// 17 | [MemberNotNullWhen(true, nameof(_firstValue))] 18 | private bool HasValue { get; } 19 | 20 | /// 21 | /// 在此命令行解析的上下文中,通常也不会为空字符串或空白字符串。 22 | /// 23 | private readonly T? _firstValue; 24 | 25 | /// 26 | /// 当所需储存的值超过 1 个时,将启用此列表。所以此列表要么为 null,要么有多于 1 个的值。 27 | /// 28 | private readonly List? _restValues; 29 | 30 | public SingleOptimizedList() 31 | { 32 | } 33 | 34 | public SingleOptimizedList(T value) 35 | { 36 | HasValue = true; 37 | _firstValue = value; 38 | } 39 | 40 | private SingleOptimizedList(T firstValue, List restValues) 41 | { 42 | HasValue = true; 43 | _firstValue = firstValue; 44 | _restValues = restValues; 45 | } 46 | 47 | /// 48 | /// 获取集合中值的个数。 49 | /// 50 | public int Count => HasValue switch 51 | { 52 | false => 0, 53 | true when _restValues is null => 1, 54 | true => _restValues.Count + 1, 55 | }; 56 | 57 | /// 58 | /// 获取集合中指定索引处的值。 59 | /// 60 | public T this[int index] => HasValue 61 | ? index is 0 ? _firstValue! : _restValues![index - 1] 62 | : throw new ArgumentOutOfRangeException(nameof(index), "集合中没有值。"); 63 | 64 | /// 65 | /// 添加一个值到集合中,并返回包含该值的新集合。 66 | /// 67 | /// 要添加的值。 68 | [Pure] 69 | public SingleOptimizedList Add(T value) 70 | { 71 | if (!HasValue) 72 | { 73 | // 空集合,添加第一个值。 74 | return new SingleOptimizedList(value); 75 | } 76 | 77 | if (_restValues is null) 78 | { 79 | // 只有一个值,添加第二个值。 80 | return new SingleOptimizedList(_firstValue, [value]); 81 | } 82 | 83 | // 已经有多个值,添加到现有的列表中。 84 | // 注意!此行为与其他任何集合都不同,会导致新旧对象共享同一个列表的引用,同时被修改!所以日常不要使用此集合。 85 | _restValues.Add(value); 86 | return new SingleOptimizedList(_firstValue, _restValues); 87 | } 88 | 89 | public SingleOptimizedList AddRange(IReadOnlyList values) 90 | { 91 | if (values.Count is 0) 92 | { 93 | return this; 94 | } 95 | 96 | if (values.Count is 1) 97 | { 98 | return Add(values[0]); 99 | } 100 | 101 | if (!HasValue) 102 | { 103 | // 空集合,添加第一个值。 104 | return new SingleOptimizedList(values[0], values.Skip(1).ToList()); 105 | } 106 | 107 | if (_restValues is null) 108 | { 109 | // 只有一个值,添加第二个值。 110 | return new SingleOptimizedList(_firstValue, values.ToList()); 111 | } 112 | 113 | // 已经有多个值,添加到现有的列表中。 114 | // 注意!此行为与其他任何集合都不同,会导致新旧对象共享同一个列表的引用,同时被修改!所以日常不要使用此集合。 115 | _restValues.AddRange(values); 116 | return new SingleOptimizedList(_firstValue, _restValues); 117 | } 118 | 119 | public IEnumerator GetEnumerator() 120 | { 121 | if (!HasValue) 122 | { 123 | yield break; 124 | } 125 | 126 | yield return _firstValue; 127 | 128 | if (_restValues is not { } restValues) 129 | { 130 | yield break; 131 | } 132 | 133 | for (var i = 0; i < restValues.Count; i++) 134 | { 135 | var value = restValues[i]; 136 | yield return value; 137 | } 138 | } 139 | 140 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 141 | } 142 | 143 | internal static class SingleOptimizedListExtensions 144 | { 145 | public static bool TryAdd(this Dictionary> dictionary, TKey key, TValue value) 146 | where TKey : notnull 147 | { 148 | if (dictionary.TryGetValue(key, out var list)) 149 | { 150 | // 已经有值了,添加到列表中。 151 | dictionary[key] = list.Add(value); 152 | return false; 153 | } 154 | 155 | // 没有值,添加一个新的值。 156 | dictionary[key] = new SingleOptimizedList(value); 157 | return true; 158 | } 159 | 160 | public static void AddOrUpdateSingle(this Dictionary> dictionary, TKey key, TValue value) 161 | where TKey : notnull 162 | { 163 | dictionary[key] = new SingleOptimizedList(value); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/CommandLineConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using DotNetCampus.Cli.Utils.Parsers; 3 | 4 | namespace DotNetCampus.Cli.Utils; 5 | 6 | /// 7 | /// 命令行参数转换器。 8 | /// 9 | internal static class CommandLineConverter 10 | { 11 | /// 12 | /// 将一整行命令转换为命令行参数数组。 13 | /// 14 | /// 一整行命令。 15 | /// 命令行参数数组。 16 | internal static IReadOnlyList SingleLineCommandLineArgsToArrayCommandLineArgs(string singleLineCommandLineArgs) 17 | { 18 | if (string.IsNullOrWhiteSpace(singleLineCommandLineArgs)) 19 | { 20 | return ImmutableArray.Empty; 21 | } 22 | 23 | List parts = []; 24 | 25 | var start = 0; 26 | var length = 0; 27 | var quoteDepth = 0; 28 | for (var i = 0; i < singleLineCommandLineArgs.Length; i++) 29 | { 30 | var c = singleLineCommandLineArgs[i]; 31 | if (quoteDepth == 0) 32 | { 33 | if (c == ' ') 34 | { 35 | if (length > 0) 36 | { 37 | parts.Add(new Range(start, start + length)); 38 | } 39 | 40 | start = i + 1; 41 | length = 0; 42 | } 43 | else if (c == '"') 44 | { 45 | quoteDepth++; 46 | } 47 | else 48 | { 49 | length++; 50 | } 51 | } 52 | else 53 | { 54 | if (c == '"') 55 | { 56 | quoteDepth--; 57 | } 58 | } 59 | } 60 | 61 | if (length > 0) 62 | { 63 | parts.Add(new Range(start, start + length)); 64 | } 65 | 66 | return [..parts.Select(part => singleLineCommandLineArgs[part])]; 67 | } 68 | 69 | public static (string? MatchedUrlScheme, CommandLineParsedResult Result) ParseCommandLineArguments( 70 | IReadOnlyList arguments, CommandLineParsingOptions? parsingOptions) 71 | { 72 | var matchedUrlScheme = arguments.Count is 1 && parsingOptions?.SchemeNames is { Length: > 0 } schemeNames 73 | ? schemeNames.FirstOrDefault(x => arguments[0].StartsWith($"{x}://", StringComparison.OrdinalIgnoreCase)) 74 | : null; 75 | 76 | ICommandLineParser parser = (matchUrlScheme: matchedUrlScheme, parsingOptions?.Style) switch 77 | { 78 | ({ } scheme, _) => new UrlStyleParser(scheme), 79 | (_, CommandLineStyle.Flexible) => new FlexibleStyleParser(), 80 | (_, CommandLineStyle.Gnu) => new GnuStyleParser(), 81 | (_, CommandLineStyle.Posix) => new PosixStyleParser(), 82 | (_, CommandLineStyle.DotNet) => new DotNetStyleParser(), 83 | (_, CommandLineStyle.PowerShell) => new PowerShellStyleParser(), 84 | _ => new FlexibleStyleParser(), 85 | }; 86 | return (matchedUrlScheme, parser.Parse(arguments)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Handlers/DictionaryCommandHandlerCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Utils.Handlers; 5 | 6 | internal sealed class DictionaryCommandHandlerCollection : ICommandHandlerCollection 7 | { 8 | private CommandObjectCreator? _defaultHandlerCreator; 9 | private readonly ConcurrentDictionary _verbHandlers = []; 10 | 11 | public void AddHandler(string? verbName, CommandObjectCreator handlerCreator) 12 | { 13 | if (verbName is null) 14 | { 15 | if (_defaultHandlerCreator is not null) 16 | { 17 | throw new InvalidOperationException($"Duplicate default handler creator. Existed: {_defaultHandlerCreator}, new: {handlerCreator}"); 18 | } 19 | _defaultHandlerCreator = handlerCreator; 20 | } 21 | else 22 | { 23 | if (!_verbHandlers.TryAdd(verbName, handlerCreator)) 24 | { 25 | throw new InvalidOperationException($"Duplicate handler with verb {verbName}. Existed: {_verbHandlers}, new: {handlerCreator}"); 26 | } 27 | } 28 | } 29 | 30 | public ICommandHandler? TryMatch(string? verb, CommandLine commandLine) 31 | { 32 | return verb is null 33 | ? (ICommandHandler?)_defaultHandlerCreator?.Invoke(commandLine) 34 | : _verbHandlers.TryGetValue(verb, out var handlerCreator) 35 | ? (ICommandHandler)handlerCreator.Invoke(commandLine) 36 | : null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Utils.Handlers; 2 | 3 | internal sealed class TaskCommandHandler( 4 | Func optionsCreator, 5 | Func> handler) : ICommandHandler 6 | where TOptions : class 7 | { 8 | private TOptions? _options; 9 | 10 | public Task RunAsync() 11 | { 12 | _options ??= optionsCreator(); 13 | if (_options is null) 14 | { 15 | throw new InvalidOperationException($"No options of type {typeof(TOptions)} were created."); 16 | } 17 | 18 | return handler(_options); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/NamingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace DotNetCampus.Cli.Utils; 4 | 5 | internal static class NamingHelper 6 | { 7 | /// 8 | /// Check if the specified is a PascalCase string. 9 | /// 10 | /// 11 | /// 12 | internal static bool CheckIsPascalCase(string value) 13 | { 14 | var first = value[0]; 15 | if (char.IsLower(first)) 16 | { 17 | return false; 18 | } 19 | 20 | var testName = MakePascalCase(value); 21 | return string.Equals(value, testName, StringComparison.Ordinal); 22 | } 23 | 24 | /// 25 | /// Check if the specified is a kebab-case string. 26 | /// 27 | /// 28 | /// 29 | internal static bool CheckIsKebabCase(string value) 30 | { 31 | var testName = MakeKebabCase(value, true, true); 32 | return string.Equals(value, testName, StringComparison.Ordinal); 33 | } 34 | 35 | internal static string MakePascalCase(string oldName) 36 | { 37 | var builder = new StringBuilder(); 38 | 39 | var isFirstLetter = true; 40 | var isWordStart = true; 41 | for (var i = 0; i < oldName.Length; i++) 42 | { 43 | var c = oldName[i]; 44 | if (!char.IsLetterOrDigit(c)) 45 | { 46 | // Append nothing because PascalCase has no special characters. 47 | isWordStart = true; 48 | continue; 49 | } 50 | 51 | if (isFirstLetter) 52 | { 53 | if (char.IsDigit(c)) 54 | { 55 | // PascalCase does not support digital as the first letter. 56 | isWordStart = true; 57 | continue; 58 | } 59 | else if (char.IsLower(c)) 60 | { 61 | // 小写字母。 62 | isFirstLetter = false; 63 | isWordStart = false; 64 | builder.Append(char.ToUpperInvariant(c)); 65 | } 66 | else if (char.IsUpper(c)) 67 | { 68 | // 大写字母。 69 | isFirstLetter = false; 70 | isWordStart = false; 71 | builder.Append(c); 72 | } 73 | else 74 | { 75 | // 无大小写,但可作为标识符的字符(对 char 来说也视为字母)。 76 | isFirstLetter = false; 77 | isWordStart = true; 78 | builder.Append(c); 79 | } 80 | } 81 | else 82 | { 83 | if (char.IsDigit(c)) 84 | { 85 | // PascalCase does not support digital as the first letter. 86 | isWordStart = true; 87 | builder.Append(c); 88 | } 89 | else if (char.IsLower(c)) 90 | { 91 | // 小写字母。 92 | builder.Append(isWordStart 93 | ? char.ToUpperInvariant(c) 94 | : c); 95 | isWordStart = false; 96 | } 97 | else if (char.IsUpper(c)) 98 | { 99 | // 大写字母。 100 | isWordStart = false; 101 | builder.Append(c); 102 | } 103 | else 104 | { 105 | // 无大小写,但可作为标识符的字符(对 char 来说也视为字母)。 106 | isWordStart = true; 107 | builder.Append(c); 108 | } 109 | } 110 | } 111 | 112 | return builder.ToString(); 113 | } 114 | 115 | /// 116 | /// 从其他命名法转换为 kebab-case 命名法。 117 | /// 118 | /// 其他命名法的名称。 119 | /// 大写字母是否是单词分隔符。例如 SampleName_Text -> Sample-Name-Text | SampleName-Text。 120 | /// 是否将所有字母转换为小写形式。例如 Sample-Name -> sample-name | Sample-Name。 121 | /// kebab-case 命名法的字符串。 122 | internal static string MakeKebabCase(string oldName, bool isUpperSeparator = true, bool toLower = true) 123 | { 124 | var builder = new StringBuilder(); 125 | 126 | var isFirstLetter = true; 127 | var isUpperLetter = false; 128 | var isSeparator = false; 129 | for (var i = 0; i < oldName.Length; i++) 130 | { 131 | var c = oldName[i]; 132 | if (!char.IsLetterOrDigit(c)) 133 | { 134 | isUpperLetter = false; 135 | // Append nothing because kebab-case has no continuous special characters. 136 | if (!isFirstLetter) 137 | { 138 | isSeparator = true; 139 | } 140 | continue; 141 | } 142 | 143 | if (isFirstLetter) 144 | { 145 | if (char.IsDigit(c)) 146 | { 147 | // kebab-case does not support digital as the first letter. 148 | isSeparator = false; 149 | } 150 | else if (char.IsUpper(c)) 151 | { 152 | // 大写字母。 153 | isFirstLetter = false; 154 | isUpperLetter = true; 155 | isSeparator = false; 156 | builder.Append(toLower ? char.ToLowerInvariant(c) : c); 157 | } 158 | else if (char.IsLower(c)) 159 | { 160 | // 小写字母。 161 | isFirstLetter = false; 162 | isUpperLetter = false; 163 | isSeparator = false; 164 | builder.Append(c); 165 | } 166 | else 167 | { 168 | isFirstLetter = false; 169 | isUpperLetter = false; 170 | builder.Append(c); 171 | } 172 | } 173 | else 174 | { 175 | if (char.IsDigit(c)) 176 | { 177 | isUpperLetter = false; 178 | isSeparator = false; 179 | builder.Append(c); 180 | } 181 | else if (char.IsUpper(c)) 182 | { 183 | if (!isUpperLetter && (isUpperSeparator || isSeparator)) 184 | { 185 | builder.Append('-'); 186 | } 187 | isUpperLetter = true; 188 | isSeparator = false; 189 | builder.Append(toLower ? char.ToLowerInvariant(c) : c); 190 | } 191 | else if (char.IsLower(c)) 192 | { 193 | if (isSeparator) 194 | { 195 | builder.Append('-'); 196 | } 197 | isUpperLetter = false; 198 | isSeparator = false; 199 | builder.Append(c); 200 | } 201 | else 202 | { 203 | if (isSeparator) 204 | { 205 | builder.Append('-'); 206 | } 207 | builder.Append(c); 208 | } 209 | } 210 | } 211 | 212 | return builder.ToString(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParsedResult.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Utils.Collections; 2 | 3 | namespace DotNetCampus.Cli.Utils.Parsers; 4 | 5 | internal readonly record struct CommandLineParsedResult( 6 | string? GuessedVerbName, 7 | OptionDictionary LongOptions, 8 | OptionDictionary ShortOptions, 9 | ReadOnlyListRange Arguments); 10 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Parsers/ICommandLineParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace DotNetCampus.Cli.Utils.Parsers; 4 | 5 | internal interface ICommandLineParser 6 | { 7 | CommandLineParsedResult Parse(IReadOnlyList commandLineArguments); 8 | } 9 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Parsers/PosixStyleParser.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Exceptions; 2 | using DotNetCampus.Cli.Utils.Collections; 3 | 4 | namespace DotNetCampus.Cli.Utils.Parsers; 5 | 6 | /// 7 | internal sealed class PosixStyleParser : ICommandLineParser 8 | { 9 | public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) 10 | { 11 | var shortOptions = new OptionDictionary(true); 12 | string? guessedVerbName = null; 13 | List arguments = []; 14 | 15 | OptionName? lastOption = null; 16 | var lastType = PosixParsedType.Start; 17 | 18 | for (var i = 0; i < commandLineArguments.Count; i++) 19 | { 20 | var commandLineArgument = commandLineArguments[i]; 21 | var result = PosixArgument.Parse(commandLineArgument, lastType); 22 | var tempLastType = lastType; 23 | lastType = result.Type; 24 | 25 | if (result.Type is PosixParsedType.VerbOrPositionalArgument) 26 | { 27 | lastOption = null; 28 | guessedVerbName = result.Value.ToString(); 29 | arguments.Add(guessedVerbName); 30 | continue; 31 | } 32 | 33 | if (result.Type is PosixParsedType.PositionalArgument 34 | or PosixParsedType.PostPositionalArgument) 35 | { 36 | lastOption = null; 37 | arguments.Add(result.Value.ToString()); 38 | continue; 39 | } 40 | 41 | if (result.Type is PosixParsedType.ShortOption) 42 | { 43 | lastOption = result.Option; 44 | shortOptions.AddOption(result.Option); 45 | continue; 46 | } 47 | 48 | if (result.Type is PosixParsedType.MultiShortOptions) 49 | { 50 | lastOption = null; 51 | foreach (var shortOption in result.Option) 52 | { 53 | shortOptions.AddOption(shortOption); 54 | } 55 | continue; 56 | } 57 | 58 | if (result.Type is PosixParsedType.OptionValue) 59 | { 60 | // 选项值,直接添加到参数列表中。 61 | var options = tempLastType switch 62 | { 63 | PosixParsedType.ShortOption => shortOptions, 64 | _ => throw new CommandLineParseException($"Argument value {result.Value.ToString()} does not belong to any option."), 65 | }; 66 | if (lastOption is { } option) 67 | { 68 | options.AddValue(option, result.Value.ToString()); 69 | lastOption = null; 70 | } 71 | continue; 72 | } 73 | 74 | if (result.Type is PosixParsedType.PositionalArgumentSeparator) 75 | { 76 | lastOption = null; 77 | } 78 | } 79 | 80 | return new CommandLineParsedResult(guessedVerbName, 81 | OptionDictionary.Empty, // POSIX 风格不支持长选项 82 | shortOptions, 83 | arguments.ToReadOnlyList()); 84 | } 85 | } 86 | 87 | internal readonly ref struct PosixArgument(PosixParsedType type) 88 | { 89 | public PosixParsedType Type { get; } = type; 90 | public OptionName Option { get; private init; } 91 | public ReadOnlySpan Value { get; private init; } 92 | 93 | public static PosixArgument Parse(string argument, PosixParsedType lastType) 94 | { 95 | var isPostPositionalArgument = lastType is PosixParsedType.PositionalArgumentSeparator or PosixParsedType.PostPositionalArgument; 96 | 97 | if (!isPostPositionalArgument && argument is "--") 98 | { 99 | // 位置参数分隔符。 100 | return new PosixArgument(PosixParsedType.PositionalArgumentSeparator); 101 | } 102 | 103 | if (!isPostPositionalArgument && argument is ['-', '-', ..]) 104 | { 105 | // POSIX 风格不支持长选项 106 | throw new CommandLineParseException($"Long options (starting with '--') are not supported in POSIX style: {argument}"); 107 | } 108 | 109 | if (!isPostPositionalArgument && argument is ['-', _, ..]) 110 | { 111 | if (argument.Length is 2) 112 | { 113 | if (!char.IsLetterOrDigit(argument[1])) 114 | { 115 | // 短选项字符必须是字母或数字。 116 | throw new CommandLineParseException($"Invalid option format at index [{argument.Length}, 1]: {argument}"); 117 | } 118 | // 单独的短选项。 119 | return new PosixArgument(PosixParsedType.ShortOption) { Option = new OptionName(argument, Range.StartAt(1)) }; 120 | } 121 | 122 | // 检查所有字符是否都是有效的选项字符 123 | for (var i = 1; i < argument.Length; i++) 124 | { 125 | if (!char.IsLetterOrDigit(argument[i])) 126 | { 127 | throw new CommandLineParseException($"Invalid option character in POSIX style: {argument[i]} in {argument}"); 128 | } 129 | } 130 | 131 | // 多个短选项,如 -abc 132 | return new PosixArgument(PosixParsedType.MultiShortOptions) { Option = new OptionName(argument, Range.StartAt(1)) }; 133 | } 134 | 135 | if (lastType is PosixParsedType.Start) 136 | { 137 | // 如果是第一个参数,则可能是或位置参数。 138 | return new PosixArgument(PosixParsedType.VerbOrPositionalArgument) { Value = argument.AsSpan() }; 139 | } 140 | 141 | if (lastType is PosixParsedType.VerbOrPositionalArgument or PosixParsedType.PositionalArgument) 142 | { 143 | // 如果上一个是位置参数,则这个也是位置参数。 144 | return new PosixArgument(PosixParsedType.PositionalArgument) { Value = argument.AsSpan() }; 145 | } 146 | 147 | if (lastType is PosixParsedType.OptionValue) 148 | { 149 | // 如果前一个已经是选项值了,那么后一个是位置参数。 150 | return new PosixArgument(PosixParsedType.PositionalArgument) { Value = argument.AsSpan() }; 151 | } 152 | 153 | if (lastType is PosixParsedType.PositionalArgumentSeparator or PosixParsedType.PostPositionalArgument) 154 | { 155 | // 如果是位置参数分隔符或后置位置参数,则必定是后置位置参数。 156 | return new PosixArgument(PosixParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; 157 | } if (lastType is PosixParsedType.MultiShortOptions) 158 | { 159 | // 在POSIX风格中,组合短选项后面不能直接跟参数值 160 | throw new CommandLineParseException($"Combined short options cannot have parameters in POSIX style: {argument}"); 161 | } 162 | 163 | // 其他情况,是单个短选项的值。 164 | return new PosixArgument(PosixParsedType.OptionValue) { Value = argument.AsSpan() }; 165 | } 166 | } 167 | 168 | internal enum PosixParsedType 169 | { 170 | /// 171 | /// 尚未开始解析。 172 | /// 173 | Start, 174 | 175 | /// 176 | /// 第一个位置参数,也可能是谓词。 177 | /// 178 | VerbOrPositionalArgument, 179 | 180 | /// 181 | /// 位置参数。 182 | /// 183 | PositionalArgument, 184 | 185 | /// 186 | /// 短选项。-o 187 | /// 188 | ShortOption, 189 | 190 | /// 191 | /// 多个短选项。-abc 192 | /// 193 | MultiShortOptions, 194 | 195 | /// 196 | /// 选项值。value 197 | /// 198 | OptionValue, 199 | 200 | /// 201 | /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 202 | /// 203 | PositionalArgumentSeparator, 204 | 205 | /// 206 | /// 后置的位置参数。 207 | /// 208 | PostPositionalArgument, 209 | } 210 | -------------------------------------------------------------------------------- /src/DotNetCampus.CommandLine/Utils/Parsers/PowerShellStyleParser.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Utils.Collections; 2 | 3 | namespace DotNetCampus.Cli.Utils.Parsers; 4 | 5 | /// 6 | internal sealed class PowerShellStyleParser : ICommandLineParser 7 | { 8 | public CommandLineParsedResult Parse(IReadOnlyList commandLineArguments) 9 | { 10 | var longOptions = new OptionDictionary(true); 11 | string? guessedVerbName = null; 12 | List arguments = []; 13 | 14 | OptionName? lastOption = null; 15 | var lastType = PowerShellParsedType.Start; 16 | 17 | for (var i = 0; i < commandLineArguments.Count; i++) 18 | { 19 | var commandLineArgument = commandLineArguments[i]; 20 | var result = PowerShellArgument.Parse(commandLineArgument, lastType); 21 | var tempLastType = lastType; 22 | lastType = result.Type; 23 | 24 | if (result.Type is PowerShellParsedType.VerbOrPositionalArgument) 25 | { 26 | lastOption = null; 27 | guessedVerbName = result.Value.ToString(); 28 | arguments.Add(guessedVerbName); 29 | continue; 30 | } 31 | 32 | if (result.Type is PowerShellParsedType.PositionalArgument 33 | or PowerShellParsedType.PostPositionalArgument) 34 | { 35 | lastOption = null; 36 | arguments.Add(result.Value.ToString()); 37 | continue; 38 | } 39 | 40 | if (result.Type is PowerShellParsedType.Option) 41 | { 42 | lastOption = result.Option; 43 | longOptions.AddOption(result.Option); 44 | continue; 45 | } 46 | 47 | if (result.Type is PowerShellParsedType.OptionValue) 48 | { 49 | // 选项值 50 | if (lastOption is { } option) 51 | { 52 | longOptions.AddValue(option, result.Value.ToString()); 53 | } 54 | continue; 55 | } 56 | 57 | if (result.Type is PowerShellParsedType.PositionalArgumentSeparator) 58 | { 59 | lastOption = null; 60 | } 61 | } 62 | 63 | return new CommandLineParsedResult(guessedVerbName, 64 | longOptions, 65 | // PowerShell 风格不使用短选项,所以直接使用空字典。 66 | OptionDictionary.Empty, 67 | arguments.ToReadOnlyList()); 68 | } 69 | } 70 | 71 | internal readonly ref struct PowerShellArgument(PowerShellParsedType type) 72 | { 73 | public PowerShellParsedType Type { get; } = type; 74 | public OptionName Option { get; private init; } 75 | public ReadOnlySpan Value { get; private init; } 76 | 77 | public static PowerShellArgument Parse(string argument, PowerShellParsedType lastType) 78 | { 79 | var isPostPositionalArgument = lastType is PowerShellParsedType.PositionalArgumentSeparator or PowerShellParsedType.PostPositionalArgument; 80 | 81 | if (!isPostPositionalArgument && argument is "--") 82 | { 83 | // 位置参数分隔符。 84 | return new PowerShellArgument(PowerShellParsedType.PositionalArgumentSeparator); 85 | } 86 | 87 | if (!isPostPositionalArgument && argument.StartsWith('-') && argument.Length > 1 && !char.IsDigit(argument[1])) 88 | { 89 | // PowerShell 风格的选项 (-ParameterName) 90 | var optionSpan = argument.AsSpan(1); 91 | return new PowerShellArgument(PowerShellParsedType.Option) 92 | { 93 | Option = new OptionName(OptionName.MakeKebabCase(optionSpan), Range.All), 94 | }; 95 | } 96 | 97 | // 处理各种类型的位置参数和选项值 98 | if (lastType is PowerShellParsedType.Start) 99 | { 100 | // 如果是第一个参数,则视为谓词或位置参数。 101 | return new PowerShellArgument(PowerShellParsedType.VerbOrPositionalArgument) { Value = argument.AsSpan() }; 102 | } 103 | 104 | if (lastType is PowerShellParsedType.VerbOrPositionalArgument or PowerShellParsedType.PositionalArgument) 105 | { 106 | // 如果前一个是位置参数,则当前也是位置参数。 107 | return new PowerShellArgument(PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan() }; 108 | } 109 | 110 | if (lastType is PowerShellParsedType.Option) 111 | { 112 | // 如果前一个是选项,则当前是选项值。 113 | return new PowerShellArgument(PowerShellParsedType.OptionValue) { Value = argument.AsSpan() }; 114 | } 115 | 116 | if (lastType is PowerShellParsedType.OptionValue) 117 | { 118 | // PowerShell 允许选项后面的多个选项值。 119 | return new PowerShellArgument(PowerShellParsedType.OptionValue) { Value = argument.AsSpan() }; 120 | } 121 | 122 | if (lastType is PowerShellParsedType.PositionalArgumentSeparator or PowerShellParsedType.PostPositionalArgument) 123 | { 124 | // 如果前一个是位置参数分隔符或后置位置参数,则当前是后置位置参数。 125 | return new PowerShellArgument(PowerShellParsedType.PostPositionalArgument) { Value = argument.AsSpan() }; 126 | } 127 | 128 | // 其他情况,都视为位置参数。 129 | return new PowerShellArgument(PowerShellParsedType.PositionalArgument) { Value = argument.AsSpan() }; 130 | } 131 | } 132 | 133 | internal enum PowerShellParsedType 134 | { 135 | /// 136 | /// 尚未开始解析。 137 | /// 138 | Start, 139 | 140 | /// 141 | /// 第一个位置参数,也可能是谓词。 142 | /// 143 | VerbOrPositionalArgument, 144 | 145 | /// 146 | /// 位置参数。 147 | /// 148 | PositionalArgument, 149 | 150 | /// 151 | /// PowerShell风格的选项。-ParameterName 152 | /// 153 | Option, 154 | 155 | /// 156 | /// 选项值。 157 | /// 158 | OptionValue, 159 | 160 | /// 161 | /// 位置参数分隔符。-- 之后的参数都被视为位置参数。 162 | /// 163 | PositionalArgumentSeparator, 164 | 165 | /// 166 | /// 后置的位置参数。 167 | /// 168 | PostPositionalArgument, 169 | } 170 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Performance/CommandLineParserTest.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.IO; 3 | using BenchmarkDotNet.Attributes; 4 | using CommandLine; 5 | using dotnetCampus.Cli; 6 | using DotNetCampus.Cli.Performance.Fakes; 7 | using DotNetCampus.Cli.Tests.Fakes; 8 | using static DotNetCampus.Cli.Tests.Fakes.CommandLineArgs; 9 | using static DotNetCampus.Cli.CommandLineParsingOptions; 10 | 11 | // ReSharper disable ReturnValueOfPureMethodIsNotUsed 12 | 13 | namespace DotNetCampus.Cli.Performance; 14 | 15 | // [DryJob] // 取消注释以验证测试能否运行。 16 | [MemoryDiagnoser] 17 | [BenchmarkCategory("CommandLine.Parse")] 18 | public class CommandLineParserTest 19 | { 20 | [Benchmark(Description = "parse [] --flexible")] 21 | public void Parse_NoArgs_Flexible() 22 | { 23 | var commandLine = CommandLine.Parse(NoArgs, Flexible); 24 | commandLine.As(); 25 | } 26 | 27 | [Benchmark(Description = "parse [] --gnu")] 28 | public void Parse_NoArgs_Gnu() 29 | { 30 | var commandLine = CommandLine.Parse(NoArgs, Gnu); 31 | commandLine.As(); 32 | } 33 | 34 | [Benchmark(Description = "parse [] --posix")] 35 | public void Parse_NoArgs_Posix() 36 | { 37 | var commandLine = CommandLine.Parse(NoArgs, Posix); 38 | commandLine.As(); 39 | } 40 | 41 | [Benchmark(Description = "parse [] --dotnet")] 42 | public void Parse_NoArgs_DotNet() 43 | { 44 | var commandLine = CommandLine.Parse(NoArgs, DotNet); 45 | commandLine.As(); 46 | } 47 | 48 | [Benchmark(Description = "parse [] --powershell")] 49 | public void Parse_NoArgs_PowerShell() 50 | { 51 | var commandLine = CommandLine.Parse(NoArgs, PowerShell); 52 | commandLine.As(); 53 | } 54 | 55 | [Benchmark(Description = "parse [] -v=3.x -p=parser")] 56 | public void Parse_NoArgs_3x_Parser() 57 | { 58 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); 59 | commandLine.As(new OptionsParser()); 60 | } 61 | 62 | [Benchmark(Description = "parse [] -v=3.x -p=runtime")] 63 | public void Parse_NoArgs_3x_Runtime() 64 | { 65 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(NoArgs); 66 | commandLine.As(); 67 | } 68 | 69 | [Benchmark(Description = "parse [PS1] --flexible")] 70 | public void Parse_PowerShell_Flexible() 71 | { 72 | var commandLine = CommandLine.Parse(WindowsStyleArgs, Flexible); 73 | commandLine.As(); 74 | } 75 | 76 | [Benchmark(Description = "parse [PS1] --dotnet")] 77 | public void Parse_PowerShell_PowerShell() 78 | { 79 | var commandLine = CommandLine.Parse(WindowsStyleArgs, DotNet); 80 | commandLine.As(); 81 | } 82 | 83 | [Benchmark(Description = "parse [PS1] -v=3.x -p=parser")] 84 | public void Parse_PowerShell_3x_Parser() 85 | { 86 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(WindowsStyleArgs); 87 | commandLine.As(new OptionsParser()); 88 | } 89 | 90 | [Benchmark(Description = "parse [PS1] -v=3.x -p=runtime")] 91 | public void Parse_PowerShell_3x_Runtime() 92 | { 93 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(WindowsStyleArgs); 94 | commandLine.As(); 95 | } 96 | 97 | [Benchmark(Description = "parse [CMD] --flexible")] 98 | public void Parse_Cmd_Flexible() 99 | { 100 | var commandLine = CommandLine.Parse(CmdStyleArgs, Flexible); 101 | commandLine.As(); 102 | } 103 | 104 | [Benchmark(Description = "parse [CMD] --dotnet")] 105 | public void Parse_Cmd_PowerShell() 106 | { 107 | var commandLine = CommandLine.Parse(CmdStyleArgs, DotNet); 108 | commandLine.As(); 109 | } 110 | 111 | [Benchmark(Description = "parse [CMD] -v=3.x -p=parser")] 112 | public void Parse_Cmd_3x_Parser() 113 | { 114 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdStyleArgs); 115 | commandLine.As(new OptionsParser()); 116 | } 117 | 118 | [Benchmark(Description = "parse [CMD] -v=3.x -p=runtime")] 119 | public void Parse_Cmd_3x_Runtime() 120 | { 121 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(CmdStyleArgs); 122 | commandLine.As(); 123 | } 124 | 125 | [Benchmark(Description = "parse [GNU] --flexible")] 126 | public void Parse_Gnu_Flexible() 127 | { 128 | var commandLine = CommandLine.Parse(LinuxStyleArgs, Flexible); 129 | commandLine.As(); 130 | } 131 | 132 | [Benchmark(Description = "parse [GNU] --gnu")] 133 | public void Parse_Gnu_Gnu() 134 | { 135 | var commandLine = CommandLine.Parse(LinuxStyleArgs, Gnu); 136 | commandLine.As(); 137 | } 138 | 139 | [Benchmark(Description = "parse [GNU] -v=3.x -p=parser")] 140 | public void Parse_Gnu_3x_Parser() 141 | { 142 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(LinuxStyleArgs); 143 | commandLine.As(new OptionsParser()); 144 | } 145 | 146 | [Benchmark(Description = "parse [GNU] -v=3.x -p=runtime")] 147 | public void Parse_Gnu_3x_Runtime() 148 | { 149 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(LinuxStyleArgs); 150 | commandLine.As(); 151 | } 152 | 153 | [Benchmark(Description = "handle [Edit,Print] --flexible")] 154 | public void Handle_Verbs_Flexible() 155 | { 156 | CommandLine.Parse(EditVerbArgs) 157 | .AddHandler(options => 0) 158 | .AddHandler(options => 0) 159 | .Run(); 160 | } 161 | 162 | [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=parser")] 163 | public void Handle_Verbs_Parser() 164 | { 165 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); 166 | commandLine 167 | .AddHandler(options => 0, new SelfWrittenEditOptionsParser()) 168 | .AddHandler(options => 0, new SelfWrittenPrintOptionsParser()) 169 | .Run(); 170 | } 171 | 172 | [Benchmark(Description = "handle [Edit,Print] -v=3.x -p=runtime")] 173 | public void Handle_Verbs_Runtime() 174 | { 175 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(EditVerbArgs); 176 | commandLine 177 | .AddHandler(options => 0) 178 | .AddHandler(options => 0) 179 | .Run(); 180 | } 181 | 182 | [Benchmark(Description = "parse [URL]")] 183 | public void Parse_Url() 184 | { 185 | var commandLine = CommandLine.Parse(UrlArgs, new CommandLineParsingOptions { SchemeNames = ["walterlv"] }); 186 | commandLine.As(); 187 | } 188 | 189 | [Benchmark(Description = "parse [URL] -v=3.x -p=parser")] 190 | public void Parse_Url_3x_Parser() 191 | { 192 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); 193 | commandLine.As(new OptionsParser()); 194 | } 195 | 196 | [Benchmark(Description = "parse [URL] -v=3.x -p=runtime")] 197 | public void Parse_Url_3x_Runtime() 198 | { 199 | var commandLine = dotnetCampus.Cli.CommandLine.Parse(UrlArgs); 200 | commandLine.As(); 201 | } 202 | 203 | [Benchmark(Description = "NuGet: CommandLineParser")] 204 | public void CommandLineParser() 205 | { 206 | Parser.Default.ParseArguments(LinuxStyleArgs).WithParsed(options => { }); 207 | } 208 | 209 | [Benchmark(Description = "NuGet: System.CommandLine")] 210 | public void SystemCommandLine() 211 | { 212 | var fileOption = new System.CommandLine.Option( 213 | name: "--file", 214 | description: "The file to read and display on the console."); 215 | 216 | var rootCommand = new RootCommand("Benchmark for System.CommandLine"); 217 | rootCommand.AddOption(fileOption); 218 | rootCommand.SetHandler(file => { }, fileOption); 219 | 220 | rootCommand.Invoke(LinuxStyleArgs); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Performance/DotNetCampus.CommandLine.Performance.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | DotNetCampus.Cli.Performance 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | dotnetCampus.CommandLine.Legacy.dll 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Performance/Fakes/ComparedOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Performance.Fakes; 5 | 6 | /// 7 | /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 8 | /// 9 | public class ComparedOptions 10 | { 11 | /// 12 | /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 13 | /// 14 | [Value(0), Option('f', "file")] 15 | public string? FilePath { get; set; } 16 | 17 | /// 18 | /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 19 | /// 20 | [Option("cloud"), DefaultValue(false)] 21 | public bool IsFromCloud { get; set; } 22 | 23 | /// 24 | /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 25 | /// 26 | [Option('m', "mode")] 27 | public string? StartupMode { get; set; } 28 | 29 | /// 30 | /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 31 | /// 32 | [Option('s', "silence"), DefaultValue(false)] 33 | public bool IsSilence { get; set; } 34 | 35 | /// 36 | /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 37 | /// 38 | [Option("iwb"), DefaultValue(false)] 39 | public bool IsIwb { get; set; } 40 | 41 | /// 42 | /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 43 | /// 44 | [Option('p', "placement")] 45 | public string? Placement { get; set; } 46 | 47 | /// 48 | /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 49 | /// 50 | [Option("startup-session")] 51 | public string? StartupSession { get; set; } 52 | } 53 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Performance/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace DotNetCampus.Cli.Performance; 5 | 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Performance/dotnetCampus.CommandLine.Legacy.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/DotNetCampus.CommandLine/eb0ee98c96dec80b95f80cedfacf3ec7e92f3246/tests/DotNetCampus.CommandLine.Performance/dotnetCampus.CommandLine.Legacy.dll -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Analyzers/OptionLongNameMustBePascalCaseAnalyzerTest.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using System.Linq; 4 | //using System.Text; 5 | //using System.Threading.Tasks; 6 | //using System.Xml; 7 | 8 | //using DotNetCampus.CommandLine; 9 | //using DotNetCampus.CommandLine.Analyzers; 10 | 11 | //using Microsoft.CodeAnalysis; 12 | //using Microsoft.CodeAnalysis.Diagnostics; 13 | //using Microsoft.VisualStudio.TestTools.UnitTesting; 14 | 15 | //using MSTest.Extensions.Contracts; 16 | 17 | //using RoslynTestKit; 18 | 19 | //namespace DotNetCampus.Cli.Tests.Analyzers 20 | //{ 21 | // [TestClass] 22 | // public class OptionLongNameMustBePascalCaseAnalyzerTest : AnalyzerTestFixture 23 | // { 24 | // protected override string LanguageName => LanguageNames.CSharp; 25 | // protected override DiagnosticAnalyzer CreateAnalyzer() => new OptionLongNameMustBePascalCaseAnalyzer(); 26 | 27 | // [ContractTestCase] 28 | // public void TestWithoutNumbers() 29 | // { 30 | // "使用 Pascal 风格的长名称,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( 31 | // "WalterlvIsAdobe", 32 | // "Walterlv"); 33 | 34 | // "使用非 Pascal 风格的长名称,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( 35 | // "--walterlv-is-adobe", 36 | // "-WalterlvIsAdobe", 37 | // "/WalterlvIsAdobe", 38 | // "walterlv-is-adobe", 39 | // "walterlvIsAdobe", 40 | // "walterlv_is_adobe", 41 | // "waltelv", 42 | // "--walterlv", 43 | // "-Walterlv", 44 | // "/Walterlv"); 45 | 46 | // "多位全大写字母,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( 47 | // "HTML", 48 | // "AddedHTMLFile"); 49 | 50 | // "两位全大写字母,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( 51 | // "IO", 52 | // "IOSetting", 53 | // "TestIO", 54 | // "TestIOSetting"); 55 | // } 56 | 57 | // [ContractTestCase] 58 | // public void TestWithNumbers() 59 | // { 60 | // "使用 Pascal 风格的长名称,不报告 Pascal 诊断。".Test(TestNoDiagnostic).WithArguments( 61 | // "Files2Build", 62 | // "Html5"); 63 | 64 | // "使用非 Pascal 风格的长名称,报告 Pascal 诊断。".Test(TestHasDiagnostic).WithArguments( 65 | // "--walterlv-is-adobe", 66 | // "-WalterlvIsAdobe", 67 | // "/WalterlvIsAdobe", 68 | // "walterlv-is-adobe", 69 | // "walterlvIsAdobe", 70 | // "walterlv_is_adobe", 71 | // "waltelv", 72 | // "--walterlv", 73 | // "-Walterlv", 74 | // "/Walterlv"); 75 | // } 76 | 77 | // private void TestHasDiagnostic(string longName) 78 | // { 79 | // string code = $@" 80 | //class Options 81 | //{{ 82 | // [Option('d', ""{longName}"")] 83 | // public string? DemoOption {{ get; set; }} 84 | //}}"; 85 | // HasDiagnostic(code, DiagnosticIds.OptionLongNameMustBePascalCase); 86 | // } 87 | 88 | // private void TestNoDiagnostic(string longName) 89 | // { 90 | // string code = $@" 91 | //class Options 92 | //{{ 93 | // [Option('d', ""{longName}"")] 94 | // public string? DemoOption {{ get; set; }} 95 | //}}"; 96 | // NoDiagnostic(code, DiagnosticIds.OptionLongNameMustBePascalCase); 97 | // } 98 | // } 99 | //} 100 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/CommandLineTests.ValueRange.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | using DotNetCampus.Cli.Tests.Fakes; 4 | 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using MSTest.Extensions.Contracts; 8 | 9 | namespace DotNetCampus.Cli.Tests 10 | { 11 | public partial class CommandLineTests 12 | { 13 | [ContractTestCase] 14 | public void ParseValues() 15 | { 16 | "命令行中包含 --,那么 -- 后的字符串完全属于值。".Test((string[] args) => 17 | { 18 | // Arrange & Action 19 | var commandLine = CommandLine.Parse(args); 20 | var options = commandLine.As(); 21 | 22 | // Assert 23 | Assert.AreEqual("foo", options.Foo); 24 | Assert.AreEqual(8, options.LongValue); 25 | CollectionAssert.AreEqual(new[] { "x", "y" }, (ICollection?)options.Values); 26 | Assert.AreEqual(2, options.Int32Value); 27 | }).WithArguments( 28 | new[] { "8", "x", "y", "2", "-f", "foo" }, 29 | new[] { "-f", "foo", "--", "8", "x", "y", "2" } 30 | ); 31 | 32 | "命令行中包含 --,那么 -- 后的字符串完全属于值,即使后面包含 -。".Test((string[] args) => 33 | { 34 | // Arrange & Action 35 | var commandLine = CommandLine.Parse(args); 36 | var options = commandLine.As(); 37 | 38 | // Assert 39 | Assert.AreEqual("foo", options.Foo); 40 | Assert.AreEqual(-8, options.LongValue); 41 | CollectionAssert.AreEqual(new[] { "-x", "-y" }, (ICollection?)options.Values); 42 | Assert.AreEqual(-2, options.Int32Value); 43 | }).WithArguments( 44 | new[] { "-f", "foo", "--", "-8", "-x", "-y", "-2" } 45 | ); 46 | 47 | "命令行中包含 --,那么 -- 后的字符串完全属于值,且完全赋值。".Test((string[] args) => 48 | { 49 | // Arrange & Action 50 | var commandLine = CommandLine.Parse(args); 51 | var options = commandLine.As(); 52 | 53 | // Assert 54 | Assert.AreEqual("foo", options.Section); 55 | Assert.AreEqual(8, options.Count); 56 | CollectionAssert.AreEqual(new[] { "dcl.exe", "--foo", "xyz", "-s", "some", "2" }, (ICollection?)options.Args); 57 | }).WithArguments( 58 | new[] { "-s", "foo", "--", "8", "dcl.exe", "--foo", "xyz", "-s", "some", "2" } 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/DotNetCampus.CommandLine.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | DotNetCampus.Cli.Tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ..\dotnetCampus.CommandLine.Performance\dotnetCampus.CommandLine.Legacy.dll 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes; 5 | 6 | public class AmbiguousOptions 7 | { 8 | /// 9 | /// 命令行中传入 --boolean 也可,传入 --boolean true 也可。 10 | /// 11 | [Option("Boolean")] 12 | public bool Boolean { get; set; } 13 | 14 | /// 15 | /// 命令行中传入 --string-boolean true 也可,会使得值为 true。 16 | /// 17 | [Option("StringBoolean")] 18 | public string? StringBoolean { get; set; } 19 | 20 | /// 21 | /// 命令行中传入 --string-array a 也可。 22 | /// 23 | [Option("StringArray")] 24 | public string? StringArray { get; set; } 25 | 26 | /// 27 | /// 命令行中传入 --string-array a 也可,传入 --string-array a b 也可。 28 | /// 29 | [Option("Array")] 30 | public IReadOnlyList? Array { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/AmbiguousOptionsParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using dotnetCampus.Cli; 8 | 9 | namespace DotNetCampus.Cli.Tests.Fakes 10 | { 11 | public class AmbiguousOptionsParser : CommandLineOptionParser 12 | { 13 | public AmbiguousOptionsParser() 14 | { 15 | bool boolean = false; 16 | string? stringBoolean = null; 17 | string? stringArray = null; 18 | IReadOnlyList? array = null; 19 | 20 | AddMatch("Boolean", value => boolean = value); 21 | AddMatch("StringBoolean", value => stringBoolean = value); 22 | AddMatch("StringArray", value => stringArray = value); 23 | AddMatch("Array", value => array = value); 24 | 25 | SetResult(() => new AmbiguousOptions() 26 | { 27 | Boolean = boolean, 28 | StringBoolean = stringBoolean, 29 | StringArray = stringArray, 30 | Array = array, 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/AssemblyCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Compiler; 2 | 3 | namespace DotNetCampus.Cli.Tests.Fakes; 4 | 5 | [CollectCommandHandlersFromThisAssembly] 6 | internal partial class AssemblyCommandHandler; 7 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/CollectionOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using DotNetCampus.Cli.Compiler; 4 | 5 | namespace DotNetCampus.Cli.Tests.Fakes; 6 | 7 | public class CollectionOptions 8 | { 9 | [Option("ReadOnlyList")] 10 | public IReadOnlyList? ReadOnlyList { get; set; } 11 | 12 | [Option("List")] 13 | public IList? List { get; set; } 14 | 15 | [Option("Collection")] 16 | public Collection? Collection { get; set; } 17 | 18 | [Option("Array")] 19 | public string[]? Array { get; set; } 20 | 21 | [Option("Enumerable")] 22 | public IEnumerable? Enumerable { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/CommandLineArgs.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Tests.Fakes; 2 | 3 | internal static class CommandLineArgs 4 | { 5 | internal const string UrlProtocol = "walterlv"; 6 | internal const string FileValue = @"C:\Users\lvyi\Desktop\文件.txt"; 7 | internal const bool CloudValue = true; 8 | internal const bool IwbValue = true; 9 | internal const string ModeValue = "Display"; 10 | internal const bool SilenceValue = true; 11 | internal const string PlacementValue = "Outside"; 12 | internal const string StartupSessionValue = "89EA9D26-6464-4E71-BD04-AA6516063D83"; 13 | 14 | internal static readonly string[] NoArgs = new string[0]; 15 | 16 | internal static readonly string[] WindowsStyleArgs = 17 | { 18 | FileValue, 19 | "-Cloud", 20 | "-Iwb", 21 | "-m", 22 | ModeValue, 23 | "-s", 24 | "-p", 25 | PlacementValue, 26 | "-StartupSession", 27 | StartupSessionValue, 28 | }; 29 | 30 | internal static readonly string[] CmdStyleArgs = 31 | { 32 | FileValue, 33 | "/Cloud", 34 | "/Iwb", 35 | "/m", 36 | ModeValue, 37 | "/s", 38 | "/p", 39 | PlacementValue, 40 | "/StartupSession", 41 | StartupSessionValue, 42 | }; 43 | 44 | internal static readonly string[] Cmd2StyleArgs = 45 | { 46 | FileValue, 47 | "/Cloud", 48 | "/Iwb", 49 | $"/m:{ModeValue}", 50 | "/s", 51 | $"/p:{PlacementValue}", 52 | $"/StartupSession:{StartupSessionValue}", 53 | }; 54 | 55 | internal static readonly string[] LinuxStyleArgs = 56 | { 57 | FileValue, 58 | "--cloud", 59 | "--iwb", 60 | "-m", 61 | ModeValue, 62 | "-s", 63 | "-p", 64 | PlacementValue, 65 | "--startup-session", 66 | StartupSessionValue, 67 | }; 68 | 69 | internal static readonly string[] UrlArgs = 70 | { 71 | @"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", 72 | }; 73 | 74 | internal static readonly string[] EditVerbArgs = 75 | { 76 | "Edit", "XXX", 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/DefaultVerbCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DotNetCampus.Cli.Compiler; 4 | 5 | namespace DotNetCampus.Cli.Tests.Fakes; 6 | 7 | public class DefaultVerbCommandHandler : ICommandHandler 8 | { 9 | [Option("Fake")] 10 | public string? Fake { get; init; } 11 | 12 | [Option("FakeProperty")] 13 | public string? FakeProperty { get; init; } 14 | 15 | [Value] 16 | public string? Argument { get; init; } 17 | 18 | public Func? Runner { get; set; } 19 | 20 | public Task RunAsync() 21 | { 22 | if (Runner is not { } runner) 23 | { 24 | throw new InvalidOperationException("No runner is set."); 25 | } 26 | 27 | return Task.FromResult(runner()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/DictionaryOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes; 5 | 6 | public class DictionaryOptions 7 | { 8 | [Option('a', "Aaa")] 9 | public IReadOnlyDictionary? Aaa { get; set; } 10 | 11 | [Option('b', "Bbb")] 12 | public IDictionary? Bbb { get; set; } 13 | 14 | [Option('c', "Ccc")] 15 | public Dictionary? Ccc { get; set; } 16 | 17 | [Option('d', "Ddd")] 18 | public KeyValuePair Ddd { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/FakeCommandOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes; 5 | 6 | public class FakeCommandOptions 7 | { 8 | [Option("Fake")] 9 | public string? Fake { get; init; } 10 | 11 | [Option("FakeProperty")] 12 | public string? FakeProperty { get; init; } 13 | 14 | [Value] 15 | public string? Argument { get; init; } 16 | 17 | public Func? Runner { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/FakeVerbCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DotNetCampus.Cli.Compiler; 4 | 5 | namespace DotNetCampus.Cli.Tests.Fakes; 6 | 7 | [Verb("Fake")] 8 | public class FakeVerbCommandHandler : ICommandHandler 9 | { 10 | [Option("Fake")] 11 | public string? Fake { get; init; } 12 | 13 | [Option("FakeProperty")] 14 | public string? FakeProperty { get; init; } 15 | 16 | [Value] 17 | public string? Argument { get; init; } 18 | 19 | public Func? Runner { get; set; } 20 | 21 | public Task RunAsync() 22 | { 23 | if (Runner is not { } runner) 24 | { 25 | throw new InvalidOperationException("No runner is set."); 26 | } 27 | 28 | return Task.FromResult(runner()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/Options.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DotNetCampus.Cli.Tests.Fakes; 4 | 5 | /// 6 | /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 7 | /// 8 | public class Options 9 | { 10 | /// 11 | /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 12 | /// 13 | [DotNetCampus.Cli.Compiler.Value(0), DotNetCampus.Cli.Compiler.Option('f', "File")] 14 | [dotnetCampus.Cli.Value(0), dotnetCampus.Cli.Option('f', "File")] 15 | public string? FilePath { get; set; } 16 | 17 | /// 18 | /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 19 | /// 20 | [DotNetCampus.Cli.Compiler.Option("Cloud")] 21 | [dotnetCampus.Cli.Option("Cloud"), DefaultValue(false)] 22 | public bool IsFromCloud { get; init; } 23 | 24 | /// 25 | /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 26 | /// 27 | [DotNetCampus.Cli.Compiler.Option('m', "Mode")] 28 | [dotnetCampus.Cli.Option('m', "Mode")] 29 | public string? StartupMode { get; init; } 30 | 31 | /// 32 | /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 33 | /// 34 | [DotNetCampus.Cli.Compiler.Option('s', "Silence")] 35 | [dotnetCampus.Cli.Option('s', "Silence"), DefaultValue(false)] 36 | public bool IsSilence { get; init; } 37 | 38 | /// 39 | /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 40 | /// 41 | [DotNetCampus.Cli.Compiler.Option("Iwb")] 42 | [dotnetCampus.Cli.Option("Iwb"), DefaultValue(false)] 43 | public bool IsIwb { get; init; } 44 | 45 | /// 46 | /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 47 | /// 48 | [DotNetCampus.Cli.Compiler.Option('p', "Placement")] 49 | [dotnetCampus.Cli.Option('p', "Placement")] 50 | public string? Placement { get; init; } 51 | 52 | /// 53 | /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 54 | /// 55 | [DotNetCampus.Cli.Compiler.Option("StartupSession")] 56 | [dotnetCampus.Cli.Option("StartupSession")] 57 | public string? StartupSession { get; init; } 58 | 59 | /// 60 | /// 创建 类的新实例。 61 | /// 62 | public Options() 63 | { 64 | } 65 | 66 | /// 67 | /// 创建 类的新实例。 68 | /// 69 | public Options( 70 | string? filePath, 71 | bool isFromCloud, 72 | string? startupMode, 73 | bool isSilence, 74 | bool isIwb, 75 | string? placement, 76 | string? startupSession) 77 | { 78 | FilePath = filePath; 79 | IsFromCloud = isFromCloud; 80 | StartupMode = startupMode; 81 | IsSilence = isSilence; 82 | IsIwb = isIwb; 83 | Placement = placement; 84 | StartupSession = startupSession; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/OptionsParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using dotnetCampus.Cli; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes 5 | { 6 | public class OptionsParser : ICommandLineOptionParser 7 | { 8 | private bool _isFromCloud; 9 | private string? _filePath; 10 | private string? _startupMode; 11 | private bool _isSilence; 12 | private bool _isIwb; 13 | private string? _placement; 14 | private string? _startupSession; 15 | 16 | public string? Verb => null; 17 | 18 | public void SetValue(IReadOnlyList values) 19 | { 20 | _filePath = values[0]; 21 | } 22 | 23 | public void SetValue(char shortName, bool value) 24 | { 25 | switch (shortName) 26 | { 27 | case 's': 28 | _isSilence = value; 29 | break; 30 | } 31 | } 32 | 33 | public void SetValue(char shortName, string value) 34 | { 35 | switch (shortName) 36 | { 37 | case 'f': 38 | _filePath = value; 39 | break; 40 | case 'm': 41 | _startupMode = value; 42 | break; 43 | case 'p': 44 | _placement = value; 45 | break; 46 | } 47 | } 48 | 49 | public void SetValue(char shortName, IReadOnlyList values) 50 | { 51 | } 52 | 53 | public void SetValue(string longName, bool value) 54 | { 55 | switch (longName) 56 | { 57 | case "Cloud": 58 | _isFromCloud = value; 59 | break; 60 | case "Silence": 61 | _isSilence = value; 62 | break; 63 | case "Iwb": 64 | _isIwb = value; 65 | break; 66 | } 67 | } 68 | 69 | public void SetValue(string longName, string value) 70 | { 71 | switch (longName) 72 | { 73 | case "File": 74 | _filePath = value; 75 | break; 76 | case "Mode": 77 | _startupMode = value; 78 | break; 79 | case "Placement": 80 | _placement = value; 81 | break; 82 | case "StartupSession": 83 | _startupSession = value; 84 | break; 85 | } 86 | } 87 | 88 | public void SetValue(string longName, IReadOnlyList values) 89 | { 90 | } 91 | 92 | public Options Commit() 93 | { 94 | return new Options(_filePath, _isFromCloud, _startupMode, _isSilence, _isIwb, _placement, _startupSession); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/PrimaryOptions.cs: -------------------------------------------------------------------------------- 1 | using DotNetCampus.Cli.Compiler; 2 | 3 | namespace DotNetCampus.Cli.Tests.Fakes; 4 | 5 | public class PrimaryOptions 6 | { 7 | [Option('a', "Byte")] 8 | public byte Aaa { get; set; } 9 | 10 | [Option('b', "Int16")] 11 | public short Bbb { get; set; } 12 | 13 | [Option('c', "UInt16")] 14 | public ushort Ccc { get; set; } 15 | 16 | [Option('d', "Int32")] 17 | public int Ddd { get; set; } 18 | 19 | [Option('e', "UInt32")] 20 | public uint Eee { get; set; } 21 | 22 | [Option('f', "Int64")] 23 | public long Fff { get; set; } 24 | 25 | [Option('g', "UInt64")] 26 | public ulong Ggg { get; set; } 27 | 28 | [Option('h', "Single")] 29 | public float Hhh { get; set; } 30 | 31 | [Option('i', "Double")] 32 | public double Iii { get; set; } 33 | 34 | [Option('j', "Decimal")] 35 | public decimal Jjj { get; set; } 36 | } 37 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeImmutableOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DotNetCampus.Cli.Tests.Fakes 4 | { 5 | /// 6 | /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 7 | /// 8 | public class RuntimeImmutableOptions 9 | { 10 | /// 11 | /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 12 | /// 13 | [Value(0), Option('f', "File")] 14 | public string FilePath { get; } 15 | 16 | /// 17 | /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 18 | /// 19 | [Option("Cloud"), DefaultValue(false)] 20 | public bool IsFromCloud { get; } 21 | 22 | /// 23 | /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 24 | /// 25 | [Option('m', "Mode")] 26 | public string StartupMode { get; } 27 | 28 | /// 29 | /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 30 | /// 31 | [Option('s', "Silence"), DefaultValue(false)] 32 | public bool IsSilence { get; } 33 | 34 | /// 35 | /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 36 | /// 37 | [Option("Iwb"), DefaultValue(false)] 38 | public bool IsIwb { get; } 39 | 40 | /// 41 | /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 42 | /// 43 | [Option('p', "Placement")] 44 | public string Placement { get; } 45 | 46 | /// 47 | /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 48 | /// 49 | [Option("StartupSession")] 50 | public string StartupSession { get; } 51 | 52 | /// 53 | /// 创建 类的新实例。 54 | /// 55 | public RuntimeImmutableOptions( 56 | string filePath, 57 | bool isFromCloud, 58 | string startupMode, 59 | bool isSilence, 60 | bool isIwb, 61 | string placement, 62 | string startupSession) 63 | { 64 | FilePath = filePath; 65 | IsFromCloud = isFromCloud; 66 | StartupMode = startupMode; 67 | IsSilence = isSilence; 68 | IsIwb = isIwb; 69 | Placement = placement; 70 | StartupSession = startupSession; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/RuntimeOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DotNetCampus.Cli.Tests.Fakes 4 | { 5 | /// 6 | /// 表示此程序在被启动的时候使用的参数信息。此类型是不可变类型,所有实例都是线程安全的。 7 | /// 8 | public class RuntimeOptions 9 | { 10 | /// 11 | /// 表示通过打开的文件路径。此属性可能为 null,但绝不会是空字符串或空白字符串。 12 | /// 13 | [Value(0), Option('f', "File")] 14 | public string? FilePath { get; set; } 15 | 16 | /// 17 | /// 当此参数值为 true 时,表示此进程是从 Cloud 端启动的 Shell 进程。此属性默认值是 false。 18 | /// 19 | [Option("Cloud"), DefaultValue(false)] 20 | public bool IsFromCloud { get; set; } 21 | 22 | /// 23 | /// 表示 Shell 端启动的模式。此属性可能为 null,但绝不会是空字符串或空白字符串。 24 | /// 25 | [Option('m', "Mode")] 26 | public string? StartupMode { get; set; } 27 | 28 | /// 29 | /// 表示当前是否是静默方式启动,通常由 Shell 启动 Cloud 时使用。此属性默认值是 false。 30 | /// 31 | [Option('s', "Silence"), DefaultValue(false)] 32 | public bool IsSilence { get; set; } 33 | 34 | /// 35 | /// 表示当前启动时需要针对 IWB 进行处理。此属性默认值是 false。 36 | /// 37 | [Option("Iwb"), DefaultValue(false)] 38 | public bool IsIwb { get; set; } 39 | 40 | /// 41 | /// 表示当前窗口启动时应该安放的位置。此属性可能为 null,但绝不会是空字符串或空白字符串。 42 | /// 43 | [Option('p', "Placement")] 44 | public string? Placement { get; set; } 45 | 46 | /// 47 | /// 表示一个启动会话 Id,用于在多个进程间同步一些信息。此属性可能为 null,但绝不会是空字符串或空白字符串。 48 | /// 49 | [Option("StartupSession")] 50 | public string? StartupSession { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/UnlimitedValueOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes; 5 | 6 | public class UnlimitedValueOptions 7 | { 8 | [Option('s', nameof(Section))] 9 | public string? Section { get; set; } 10 | 11 | [Value(0)] 12 | public int Count { get; set; } 13 | 14 | [Value(1, int.MaxValue)] 15 | public IEnumerable? Args { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/ValueOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DotNetCampus.Cli.Compiler; 3 | 4 | namespace DotNetCampus.Cli.Tests.Fakes; 5 | 6 | public class ValueOptions 7 | { 8 | [Option('f', nameof(Foo))] 9 | public string? Foo { get; set; } 10 | 11 | [Value(0)] 12 | public long LongValue { get; set; } 13 | 14 | [Value(1, 2)] 15 | public IReadOnlyList? Values { get; set; } 16 | 17 | [Value(2)] 18 | public int Int32Value { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/Fakes/VerbOptions.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Tests.Fakes; 2 | 3 | [dotnetCampus.Cli.Verb("Edit")] 4 | [DotNetCampus.Cli.Compiler.Verb("Edit")] 5 | public class EditOptions 6 | { 7 | [dotnetCampus.Cli.Value(0), dotnetCampus.Cli.Option('f', "File")] 8 | [DotNetCampus.Cli.Compiler.Value(0), DotNetCampus.Cli.Compiler.Option('f', "File")] 9 | public string? FilePath { get; set; } 10 | } 11 | 12 | [dotnetCampus.Cli.Verb("Print")] 13 | [DotNetCampus.Cli.Compiler.Verb("Print")] 14 | public class PrintOptions 15 | { 16 | [DotNetCampus.Cli.Compiler.Value(0), Compiler.Option('f', "File")] 17 | public string? FilePath { get; set; } 18 | 19 | [DotNetCampus.Cli.Compiler.Option('p', "Printer")] 20 | public string? Printer { get; set; } 21 | } 22 | 23 | [dotnetCampus.Cli.Verb("Share")] 24 | [DotNetCampus.Cli.Compiler.Verb("Share")] 25 | public class ShareOptions 26 | { 27 | [DotNetCampus.Cli.Compiler.Option('t', "Target")] 28 | public string? Target { get; set; } 29 | } 30 | 31 | public class SelfWrittenEditOptionsParser : dotnetCampus.Cli.CommandLineOptionParser 32 | { 33 | public SelfWrittenEditOptionsParser() 34 | { 35 | var options = new EditOptions(); 36 | Verb = "Edit"; 37 | AddMatch(0, value => options.FilePath = value); 38 | AddMatch('f', value => options.FilePath = value); 39 | AddMatch("File", value => options.FilePath = value); 40 | SetResult(() => options); 41 | } 42 | } 43 | 44 | public class SelfWrittenPrintOptionsParser : dotnetCampus.Cli.CommandLineOptionParser 45 | { 46 | public SelfWrittenPrintOptionsParser() 47 | { 48 | var options = new PrintOptions(); 49 | Verb = "Print"; 50 | AddMatch(0, value => options.FilePath = value); 51 | AddMatch('f', value => options.FilePath = value); 52 | AddMatch("File", value => options.FilePath = value); 53 | AddMatch('p', value => options.Printer = value); 54 | AddMatch("Printer", value => options.Printer = value); 55 | SetResult(() => options); 56 | } 57 | } 58 | 59 | public class SelfWrittenShareOptionsParser : dotnetCampus.Cli.CommandLineOptionParser 60 | { 61 | public SelfWrittenShareOptionsParser() 62 | { 63 | var options = new ShareOptions(); 64 | Verb = "Share"; 65 | AddMatch('t', value => options.Target = value); 66 | AddMatch("Target", value => options.Target = value); 67 | SetResult(() => options); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/DotNetCampus.CommandLine.Tests/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetCampus.Cli.Tests; 2 | 3 | internal enum LogLevel 4 | { 5 | Debug, 6 | Info, 7 | Warning, 8 | Error, 9 | Critical 10 | } 11 | --------------------------------------------------------------------------------