├── Icon.png ├── global.json ├── .dockerignore ├── sandbox ├── GeneratorSandbox │ ├── appsettings.json │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── GeneratorSandbox.csproj │ ├── Temp.cs │ └── Filters.cs ├── CliFrameworkBenchmark │ ├── README.md │ ├── Properties │ │ └── launchSettings.json │ ├── Commands │ │ ├── CoconaCommand.cs │ │ ├── CommandLineParserCommand.cs │ │ ├── CliprCommand.cs │ │ ├── McMasterCommand.cs │ │ ├── PowerArgsCommand.cs │ │ ├── CliFxCommand.cs │ │ ├── SpectreConsoleCliCommand.cs │ │ ├── ConsoleAppFrameworkCommand.cs │ │ └── SystemCommandLineCommand.cs │ ├── Program.cs │ ├── CliFrameworkBenchmark.csproj │ └── Benchmark.cs ├── NativeAot │ ├── Program.cs │ └── NativeAot.csproj └── FilterShareProject │ ├── FilterShareProject.csproj │ └── Class1.cs ├── src ├── ConsoleAppFramework │ ├── Properties │ │ └── launchSettings.json │ ├── SourceGeneratorContexts.cs │ ├── IgnoreEquality.cs │ ├── StringExtensions.cs │ ├── NameConverter.cs │ ├── EquatableArray.cs │ ├── ConsoleAppFramework.csproj │ ├── EquatableTypeSymbol.cs │ ├── WellKnownTypes.cs │ ├── SourceBuilder.cs │ ├── DiagnosticDescriptors.cs │ ├── RoslynExtensions.cs │ └── CommandHelpBuilder.cs ├── ConsoleAppFramework.Abstractions │ ├── ConsoleAppFramework.Abstractions.props │ ├── ConsoleAppFramework.Abstractions.csproj │ └── ConsoleApp.Abstractions.cs └── ConsoleAppFramework.CliSchema │ ├── ConsoleAppFramework.CliSchema.csproj │ └── CommandHelpDefinition.cs ├── .github ├── workflows │ ├── stale.yaml │ ├── build-debug.yaml │ └── build-release.yaml └── dependabot.yaml ├── exclusion.dic ├── tests ├── ConsoleAppFramework.GeneratorTests │ ├── ConsoleAppFramework.GeneratorTests.csproj │ ├── RegisterCommandsTest.cs │ ├── GeneratorOptionsTest.cs │ ├── ArrayParseTest.cs │ ├── NameConverterTest.cs │ ├── DITest.cs │ ├── ArgumentParserTest.cs │ ├── BuildCustomDelegateTest.cs │ ├── ConsoleAppContextTest.cs │ ├── HiddenAttributeTest.cs │ ├── SubCommandTest.cs │ ├── FilterTest.cs │ ├── GlobalOptionTest.cs │ ├── ConsoleAppBuilderTest.cs │ ├── RunTest.cs │ ├── CSharpGeneratorRunner.cs │ ├── HelpTest.cs │ └── DiagnosticsTest.cs └── ConsoleAppFramework.NativeAotTests │ ├── ConsoleAppFramework.NativeAotTests.csproj │ └── NativeAotTest.cs ├── Directory.Build.props ├── LICENSE ├── .editorconfig ├── ConsoleAppFramework.slnx └── .gitignore /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cysharp/ConsoleAppFramework/HEAD/Icon.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "runner": "Microsoft.Testing.Platform" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Position": { 3 | "Title": "Editor", 4 | "Name": "Joe Smith" 5 | }, 6 | "MyKey": "My appsettings.json Value", 7 | "AllowedHosts": "*" 8 | } -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/README.md: -------------------------------------------------------------------------------- 1 | This benchmark project is based on CliFx.Benchmarks and Cocona Benchmarks. 2 | 3 | https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ 4 | 5 | https://github.com/mayuki/Cocona/tree/master/perf/Cocona.Benchmark.External 6 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Roslyn Debug Profile": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\..\\sandbox\\GeneratorSandbox\\GeneratorSandbox.csproj" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS 4 | 5 | 6 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/Program.cs: -------------------------------------------------------------------------------- 1 | using ConsoleAppFramework; 2 | 3 | args = ["write", "--help"]; 4 | 5 | var app = ConsoleApp.Create(); 6 | 7 | app.Add("write", (Target target) => { }); 8 | 9 | app.Run(args); 10 | 11 | public enum Target 12 | { 13 | File, 14 | Network 15 | } 16 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Default": { 4 | "commandName": "Project", 5 | "commandLineArgs": "" 6 | }, 7 | "Measure": { 8 | "commandName": "Project", 9 | "commandLineArgs": "--launchCount 20" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | stale: 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | issues: write 14 | uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main 15 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/SourceGeneratorContexts.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework; 2 | 3 | readonly record struct ConsoleAppFrameworkGeneratorOptions(bool DisableNamingConversion); 4 | 5 | readonly record struct DllReference(bool HasDependencyInjection, bool HasLogging, bool HasConfiguration, bool HasJsonConfiguration, bool HasHost, bool HasCliSchema); 6 | -------------------------------------------------------------------------------- /sandbox/NativeAot/Program.cs: -------------------------------------------------------------------------------- 1 | using ConsoleAppFramework; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | using System.Text.Json; 5 | 6 | var app = ConsoleApp.Create(); 7 | 8 | ConsoleApp.Run(args, (int x, Kabayaki y) => Console.WriteLine(x + y.MyProperty)); 9 | 10 | app.Run(args); 11 | 12 | public class Kabayaki 13 | { 14 | public int MyProperty { get; set; } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CliFrameworkBenchmarks.Commands; 2 | 3 | public class CoconaCommand 4 | { 5 | public void Execute( 6 | [Cocona.Option("str", new []{'s'})] 7 | string? strOption, 8 | [Cocona.Option("int", new []{'i'})] 9 | int intOption, 10 | [Cocona.Option("bool", new []{'b'})] 11 | bool boolOption) 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CliFrameworkBenchmarks.Commands; 2 | 3 | public class CommandLineParserCommand 4 | { 5 | [CommandLine.Option('s', "str")] 6 | public string? StrOption { get; set; } 7 | 8 | [CommandLine.Option('i', "int")] 9 | public int IntOption { get; set; } 10 | 11 | [CommandLine.Option('b', "bool")] 12 | public bool BoolOption { get; set; } 13 | 14 | public void Execute() 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sandbox/FilterShareProject/FilterShareProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Debug;Release 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs: -------------------------------------------------------------------------------- 1 | using clipr; 2 | 3 | namespace CliFrameworkBenchmarks.Commands; 4 | 5 | public class CliprCommand 6 | { 7 | [NamedArgument('s', "str")] 8 | public string? StrOption { get; set; } 9 | 10 | [NamedArgument('i', "int")] 11 | public int IntOption { get; set; } 12 | 13 | [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] 14 | public bool BoolOption { get; set; } 15 | 16 | public void Execute() 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CliFrameworkBenchmarks.Commands; 2 | 3 | public class McMasterCommand 4 | { 5 | [McMaster.Extensions.CommandLineUtils.Option("--str|-s")] 6 | public string? StrOption { get; set; } 7 | 8 | [McMaster.Extensions.CommandLineUtils.Option("--int|-i")] 9 | public int IntOption { get; set; } 10 | 11 | [McMaster.Extensions.CommandLineUtils.Option("--bool|-b")] 12 | public bool BoolOption { get; set; } 13 | 14 | public int OnExecute() => 0; 15 | } 16 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Default": { 4 | "commandName": "Project", 5 | "commandLineArgs": "" 6 | }, 7 | "ShowVersion": { 8 | "commandName": "Project", 9 | "commandLineArgs": "--version" 10 | }, 11 | "ShowRootCommandHelp": { 12 | "commandName": "Project", 13 | "commandLineArgs": "--help" 14 | }, 15 | "ShowRunCommandHelp": { 16 | "commandName": "Project", 17 | "commandLineArgs": "run --help" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /exclusion.dic: -------------------------------------------------------------------------------- 1 | abcd 2 | abcde 3 | abcdefg 4 | aiueo 5 | appsettings 6 | args 7 | authed 8 | awaitable 9 | Awaiter 10 | Binded 11 | Clipr 12 | Cysharp 13 | Decr 14 | dest 15 | Equatable 16 | fooa 17 | foobarbaz 18 | generatortest 19 | hoge 20 | ignorecase 21 | Impl 22 | Incr 23 | Kabayaki 24 | Kokuban 25 | Lamda 26 | Moge 27 | nomsg 28 | nomunomu 29 | Numerics 30 | Parsable 31 | posix 32 | saas 33 | Spectre 34 | stackalloc 35 | stdout 36 | Tacommands 37 | tako 38 | takoyaki 39 | Withargs 40 | Yaki 41 | Zeroargs 42 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs: -------------------------------------------------------------------------------- 1 | using PowerArgs; 2 | 3 | namespace CliFrameworkBenchmarks.Commands; 4 | 5 | //public class PowerArgsCommand 6 | //{ 7 | // [ArgShortcut("--str"), ArgShortcut("-s")] 8 | // public string? StrOption { get; set; } 9 | 10 | // [ArgShortcut("--int"), ArgShortcut("-i")] 11 | // public int IntOption { get; set; } 12 | 13 | // [ArgShortcut("--bool"), ArgShortcut("-b")] 14 | // public bool BoolOption { get; set; } 15 | 16 | // public void Main() 17 | // { 18 | // } 19 | //} 20 | -------------------------------------------------------------------------------- /sandbox/FilterShareProject/Class1.cs: -------------------------------------------------------------------------------- 1 | using ConsoleAppFramework; 2 | 3 | namespace FilterShareProject; 4 | 5 | public class TakoFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 6 | { 7 | public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 8 | { 9 | Console.WriteLine("TAKO"); 10 | return Next.InvokeAsync(context, cancellationToken); 11 | } 12 | } 13 | 14 | public class OtherProjectCommand 15 | { 16 | public void Execute(int x) 17 | { 18 | Console.WriteLine("Hello?"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs: -------------------------------------------------------------------------------- 1 | using CliFx.Attributes; 2 | using CliFx.Infrastructure; 3 | 4 | namespace CliFrameworkBenchmarks.Commands; 5 | 6 | [Command] 7 | public class CliFxCommand : CliFx.ICommand 8 | { 9 | [CommandOption("str", 's')] 10 | public string? StrOption { get; set; } 11 | 12 | [CommandOption("int", 'i')] 13 | public int IntOption { get; set; } 14 | 15 | [CommandOption("bool", 'b')] 16 | public bool BoolOption { get; set; } 17 | 18 | public ValueTask ExecuteAsync(IConsole console) => ValueTask.CompletedTask; 19 | } 20 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework.CliSchema/ConsoleAppFramework.CliSchema.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net10.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | true 11 | ConsoleAppFramework.CliSchema 12 | ConsoleAppFramework cli-schema metadata library. 13 | Debug;Release 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/IgnoreEquality.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework; 2 | 3 | public readonly struct IgnoreEquality(T value) : IEquatable> 4 | { 5 | public readonly T Value => value; 6 | 7 | public static implicit operator IgnoreEquality(T value) 8 | { 9 | return new IgnoreEquality(value); 10 | } 11 | 12 | public static implicit operator T(IgnoreEquality value) 13 | { 14 | return value.Value; 15 | } 16 | 17 | public bool Equals(IgnoreEquality other) 18 | { 19 | // always true to ignore equality check. 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework; 2 | 3 | internal static class StringExtensions 4 | { 5 | #if NETSTANDARD2_0 6 | public static string ReplaceLineEndings(this string input) 7 | { 8 | #pragma warning disable RS1035 9 | return ReplaceLineEndings(input, Environment.NewLine); 10 | #pragma warning restore RS1035 11 | } 12 | 13 | public static string ReplaceLineEndings(this string text, string replacementText) 14 | { 15 | text = text.Replace("\r\n", "\n"); 16 | 17 | if (replacementText != "\n") 18 | text = text.Replace("\n", replacementText); 19 | 20 | return text; 21 | } 22 | #endif 23 | } 24 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" # Check for updates to GitHub Actions every week 8 | cooldown: 9 | default-days: 14 # Wait 14 days before creating another PR for the same dependency. This will prevent vulnerability on the package impact. 10 | ignore: 11 | # I just want update action when major/minor version is updated. patch updates are too noisy. 12 | - dependency-name: "*" 13 | update-types: 14 | - version-update:semver-patch 15 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net10.0 5 | enable 6 | enable 7 | 8 | 9 | true 10 | ConsoleAppFramework.Abstractions 11 | ConsoleAppFramework external abstractions library. 12 | Debug;Release 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Cli; 2 | 3 | namespace CliFrameworkBenchmarks.Commands; 4 | 5 | public class SpectreConsoleCliCommand : Command 6 | { 7 | public sealed class Settings : CommandSettings 8 | { 9 | [CommandOption("-s")] 10 | public string? strOption { get; init; } 11 | 12 | [CommandOption("-i")] 13 | public int intOption { get; init; } 14 | 15 | [CommandOption("-b")] 16 | public bool boolOption { get; init; } 17 | } 18 | 19 | public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) 20 | { 21 | return 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | true 9 | Debug;Release 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sandbox/NativeAot/NativeAot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | true 10 | true 11 | true 12 | true 13 | true 14 | 15 | Debug;Release 16 | 17 | 18 | 19 | 20 | Analyzer 21 | false 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/NameConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | public static class NameConverter 6 | { 7 | public static string ToKebabCase(string name) 8 | { 9 | var sb = new StringBuilder(); 10 | for (int i = 0; i < name.Length; i++) 11 | { 12 | if (!Char.IsUpper(name[i])) 13 | { 14 | sb.Append(name[i]); 15 | continue; 16 | } 17 | 18 | // Abc, abC, AB-c => first or Last or capital continuous, no added. 19 | if (i == 0 || i == name.Length - 1 || Char.IsUpper(name[i + 1])) 20 | { 21 | sb.Append(Char.ToLowerInvariant(name[i])); 22 | continue; 23 | } 24 | 25 | // others, add- 26 | sb.Append('-'); 27 | sb.Append(Char.ToLowerInvariant(name[i])); 28 | } 29 | return sb.ToString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build-debug.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Debug 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "master" 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build-dotnet: 14 | permissions: 15 | contents: read 16 | runs-on: ubuntu-24.04 17 | timeout-minutes: 10 18 | steps: 19 | - uses: Cysharp/Actions/.github/actions/checkout@main 20 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main 21 | - run: dotnet build -c Release 22 | - run: dotnet test -c Release --no-build 23 | 24 | # Native AOT tests 25 | - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj 26 | - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests 27 | 28 | - run: dotnet pack -c Release --no-build -p:IncludeSymbols=true -o $GITHUB_WORKSPACE/artifacts 29 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | $(Version) 6 | Cysharp 7 | Cysharp 8 | © Cysharp, Inc. 9 | https://github.com/Cysharp/ConsoleAppFramework 10 | README.md 11 | $(PackageProjectUrl) 12 | git 13 | batch,console,cli,consoleappframework 14 | MIT 15 | Icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | true 9 | true 10 | true 11 | true 12 | true 13 | false 14 | true 15 | 16 | 17 | 18 | 19 | 20 | Analyzer 21 | false 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cysharp, Inc. 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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Visual Studio Spell checker configs (https://learn.microsoft.com/en-us/visualstudio/ide/text-spell-checker?view=vs-2022#how-to-customize-the-spell-checker) 13 | spelling_exclusion_path = ./exclusion.dic 14 | 15 | [*.cs] 16 | indent_size = 4 17 | charset = utf-8-bom 18 | end_of_line = unset 19 | 20 | # Solution files 21 | [*.{sln,slnx}] 22 | end_of_line = unset 23 | 24 | # MSBuild project files 25 | [*.{csproj,props,targets}] 26 | end_of_line = unset 27 | 28 | # Xml config files 29 | [*.{ruleset,config,nuspec,resx,runsettings,DotSettings}] 30 | end_of_line = unset 31 | 32 | # C# code style settings 33 | [*.{cs}] 34 | dotnet_diagnostic.IDE0044.severity = none # IDE0044: Make field readonly 35 | 36 | # https://stackoverflow.com/questions/79195382/how-to-disable-fading-unused-methods-in-visual-studio-2022-17-12-0 37 | dotnet_diagnostic.IDE0051.severity = none # IDE0051: Remove unused private member 38 | dotnet_diagnostic.IDE0130.severity = none # IDE0130: Namespace does not match folder structure 39 | -------------------------------------------------------------------------------- /ConsoleAppFramework.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CliFrameworkBenchmarks.Commands; 2 | //public class ConsoleAppFrameworkCommand : ConsoleAppBase 3 | //{ 4 | // public void Execute( 5 | // [global::ConsoleAppFramework.Option("s")] 6 | // string? str, 7 | // [global::ConsoleAppFramework.Option("i")] 8 | // int intOption, 9 | // [global::ConsoleAppFramework.Option("b")] 10 | // bool boolOption) 11 | // { 12 | // } 13 | //} 14 | 15 | public class ConsoleAppFrameworkCommand 16 | { 17 | /// 18 | /// 19 | /// 20 | /// -s 21 | /// -i 22 | /// -b 23 | public static void Execute(string? str, int intOption, bool boolOption) 24 | { 25 | 26 | } 27 | 28 | /// 29 | /// 30 | /// 31 | /// -s 32 | /// -i 33 | /// -b 34 | public static Task ExecuteWithCancellationToken(string? str, int intOption, bool boolOption, CancellationToken cancellationToken) 35 | { 36 | return Task.CompletedTask; 37 | } 38 | } 39 | 40 | //internal class NopConsoleAppFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 41 | //{ 42 | // public override Task InvokeAsync(CancellationToken cancellationToken) 43 | // { 44 | // return Next.InvokeAsync(cancellationToken); 45 | // } 46 | //} 47 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/EquatableArray.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ConsoleAppFramework; 5 | 6 | public readonly struct EquatableArray : IEquatable>, IEnumerable 7 | where T : IEquatable 8 | { 9 | readonly T[]? array; 10 | 11 | public EquatableArray() // for collection literal [] 12 | { 13 | array = []; 14 | } 15 | 16 | public EquatableArray(T[] array) 17 | { 18 | this.array = array; 19 | } 20 | 21 | public static implicit operator EquatableArray(T[] array) 22 | { 23 | return new EquatableArray(array); 24 | } 25 | 26 | public ref readonly T this[int index] 27 | { 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | get => ref array![index]; 30 | } 31 | 32 | public int Length => array!.Length; 33 | 34 | public ReadOnlySpan AsSpan() 35 | { 36 | return array.AsSpan(); 37 | } 38 | 39 | public ReadOnlySpan.Enumerator GetEnumerator() 40 | { 41 | return AsSpan().GetEnumerator(); 42 | } 43 | 44 | IEnumerator IEnumerable.GetEnumerator() 45 | { 46 | return array.AsEnumerable().GetEnumerator(); 47 | } 48 | 49 | IEnumerator IEnumerable.GetEnumerator() 50 | { 51 | return array.AsEnumerable().GetEnumerator(); 52 | } 53 | 54 | public bool Equals(EquatableArray other) 55 | { 56 | return AsSpan().SequenceEqual(other.AsSpan()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | 3 | namespace CliFrameworkBenchmarks.Commands; 4 | 5 | public class SystemCommandLineCommand 6 | { 7 | public static int ParseInvoke(string[] args) 8 | { 9 | var stringOption = new Option("--str", "-s"); 10 | var intOption = new Option("--int", "-i"); 11 | var boolOption = new Option("--bool", "-b"); 12 | 13 | var command = new RootCommand { stringOption, intOption, boolOption }; 14 | 15 | command.SetAction(parseResult => 16 | { 17 | _ = parseResult.GetValue(stringOption); 18 | _ = parseResult.GetValue(intOption); 19 | _ = parseResult.GetValue(boolOption); 20 | }); 21 | 22 | return command.Parse(args).Invoke(); 23 | } 24 | 25 | public static Task ParseInvokeAsync(string[] args) 26 | { 27 | var stringOption = new Option("--str", "-s"); 28 | var intOption = new Option("--int", "-i"); 29 | var boolOption = new Option("--bool", "-b"); 30 | 31 | var command = new RootCommand { stringOption, intOption, boolOption }; 32 | 33 | command.SetAction((parseResult, cancellationToken) => 34 | { 35 | _ = parseResult.GetValue(stringOption); 36 | _ = parseResult.GetValue(intOption); 37 | _ = parseResult.GetValue(boolOption); 38 | return Task.CompletedTask; 39 | }); 40 | 41 | return command.Parse(args).InvokeAsync(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | // This benchmark project is based on CliFx.Benchmarks. 2 | // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ 3 | 4 | using BenchmarkDotNet.Configs; 5 | using BenchmarkDotNet.Diagnosers; 6 | using BenchmarkDotNet.Engines; 7 | using BenchmarkDotNet.Jobs; 8 | using BenchmarkDotNet.Reports; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Toolchains.CsProj; 11 | using Perfolizer.Horology; 12 | 13 | namespace CliFrameworkBenchmarks; 14 | 15 | class Program 16 | { 17 | static void Main(string[] args) 18 | { 19 | var config = DefaultConfig.Instance 20 | .WithSummaryStyle(SummaryStyle.Default 21 | .WithTimeUnit(TimeUnit.Millisecond)) 22 | .HideColumns(BenchmarkDotNet.Columns.Column.Error) 23 | ; 24 | 25 | config.AddDiagnoser(MemoryDiagnoser.Default); 26 | // config.AddDiagnoser(new ThreadingDiagnoser(new ThreadingDiagnoserConfig(displayLockContentionWhenZero: false, displayCompletedWorkItemCountWhenZero: false))); 27 | 28 | config.AddJob(Job.Default 29 | .WithStrategy(RunStrategy.ColdStart) 30 | .WithLaunchCount(1) 31 | .WithWarmupCount(0) 32 | .WithIterationCount(1) 33 | .WithInvocationCount(1) 34 | .WithToolchain(CsProjCoreToolchain.NetCoreApp10_0) // .NET 10 35 | .DontEnforcePowerPlan()); 36 | 37 | BenchmarkRunner.Run(config, args); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | 14 8 | annotations 9 | true 10 | Debug;Release 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Analyzer 36 | false 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: "tag: git tag you want create. (sample 1.0.0)" 8 | required: true 9 | dry-run: 10 | description: "dry-run: true will never create release/nuget." 11 | required: true 12 | default: false 13 | type: boolean 14 | 15 | jobs: 16 | build-dotnet: 17 | permissions: 18 | contents: read 19 | runs-on: ubuntu-24.04 20 | timeout-minutes: 10 21 | steps: 22 | - uses: Cysharp/Actions/.github/actions/checkout@main 23 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main 24 | - run: dotnet build -c Release -p:Version=${{ inputs.tag }} 25 | - run: dotnet test -c Release --no-build 26 | # Native AOT tests 27 | - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj 28 | - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests 29 | # pack nuget 30 | - run: dotnet pack -c Release --no-build -p:Version=${{ inputs.tag }} -o ./publish 31 | - uses: Cysharp/Actions/.github/actions/upload-artifact@main 32 | with: 33 | name: nuget 34 | path: ./publish 35 | retention-days: 1 36 | 37 | # release 38 | create-release: 39 | needs: [build-dotnet] 40 | permissions: 41 | contents: write 42 | id-token: write # required for NuGet Trusted Publish 43 | uses: Cysharp/Actions/.github/workflows/create-release.yaml@main 44 | with: 45 | commit-id: ${{ github.sha }} 46 | dry-run: ${{ inputs.dry-run }} 47 | tag: ${{ inputs.tag }} 48 | nuget-push: true 49 | secrets: inherit 50 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/RegisterCommandsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleAppFramework.GeneratorTests; 8 | 9 | public class RegisterCommandsTest 10 | { 11 | readonly VerifyHelper verifier = new("CAF"); 12 | 13 | [Test] 14 | public async Task VerifyDuplicate() 15 | { 16 | await verifier.Verify(7, """ 17 | var app = ConsoleApp.Create(); 18 | app.Run(args); 19 | 20 | [RegisterCommands] 21 | public class Foo 22 | { 23 | public async Task Bar(int x) 24 | { 25 | Console.Write(x); 26 | } 27 | 28 | public async Task Baz(int y) 29 | { 30 | Console.Write(y); 31 | } 32 | } 33 | 34 | [RegisterCommands] 35 | public class Hoge 36 | { 37 | public async Task Bar(int x) 38 | { 39 | Console.Write(x); 40 | } 41 | 42 | public async Task Baz(int y) 43 | { 44 | Console.Write(y); 45 | } 46 | } 47 | """, "Bar"); 48 | } 49 | 50 | [Test] 51 | public async Task Exec() 52 | { 53 | var code = """ 54 | var app = ConsoleApp.Create(); 55 | app.Run(args); 56 | 57 | [RegisterCommands] 58 | public class Foo 59 | { 60 | public async Task Bar(int x) 61 | { 62 | Console.Write(x); 63 | } 64 | 65 | public async Task Baz(int y) 66 | { 67 | Console.Write(y); 68 | } 69 | } 70 | 71 | [RegisterCommands("hoge")] 72 | public class Hoge 73 | { 74 | public async Task Bar(int x) 75 | { 76 | Console.Write(x); 77 | } 78 | 79 | public async Task Baz(int y) 80 | { 81 | Console.Write(y); 82 | } 83 | } 84 | """; 85 | 86 | await verifier.Execute(code, "bar --x 10", "10"); 87 | await verifier.Execute(code, "baz --y 20", "20"); 88 | await verifier.Execute(code, "hoge bar --x 10", "10"); 89 | await verifier.Execute(code, "hoge baz --y 20", "20"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleAppFramework.GeneratorTests; 8 | 9 | public class GeneratorOptionsTest 10 | { 11 | VerifyHelper verifier = new VerifyHelper("CAF"); 12 | 13 | [Test] 14 | public async Task DisableNamingConversionRun() 15 | { 16 | await verifier.Execute(""" 17 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] 18 | 19 | ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); }); 20 | """, args: "--fooBarBaz 100", expected: "100"); 21 | 22 | await verifier.Execute(""" 23 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)] 24 | 25 | ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); }); 26 | """, args: "--foo-bar-baz 100", expected: "100"); 27 | } 28 | 29 | [Test] 30 | public async Task DisableNamingConversionBuilder() 31 | { 32 | await verifier.Execute(""" 33 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] 34 | 35 | var app = ConsoleApp.Create(); 36 | app.Add(); 37 | app.Run(args); 38 | 39 | class Commands 40 | { 41 | public async Task FooBarBaz(int hogeMoge, int takoYaki) 42 | { 43 | Console.Write(hogeMoge + takoYaki); 44 | } 45 | } 46 | """, args: "FooBarBaz --hogeMoge 100 --takoYaki 200", expected: "300"); 47 | 48 | await verifier.Execute(""" 49 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)] 50 | 51 | var app = ConsoleApp.Create(); 52 | app.Add(); 53 | app.Run(args); 54 | 55 | class Commands 56 | { 57 | public async Task FooBarBaz(int hogeMoge, int takoYaki) 58 | { 59 | Console.Write(hogeMoge + takoYaki); 60 | } 61 | } 62 | """, args: "foo-bar-baz --hoge-moge 100 --tako-yaki 200", expected: "300"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/ConsoleAppFramework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 12 6 | enable 7 | enable 8 | ConsoleAppFramework 9 | true 10 | cs 11 | 12 | 13 | false 14 | true 15 | false 16 | true 17 | true 18 | 19 | 20 | true 21 | ConsoleAppFramework 22 | Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. 23 | Debug;Release 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | all 38 | runtime; build; native; contentfiles; analyzers; buildtransitive 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework.CliSchema/CommandHelpDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | public record CommandHelpDefinition 6 | { 7 | public string CommandName { get; } 8 | public CommandOptionHelpDefinition[] Options { get; } 9 | public string Description { get; } 10 | 11 | public CommandHelpDefinition(string commandName, CommandOptionHelpDefinition[] options, string description) 12 | { 13 | CommandName = commandName; 14 | Options = options; 15 | Description = description; 16 | } 17 | } 18 | 19 | public record CommandOptionHelpDefinition 20 | { 21 | public string[] Options { get; } 22 | public string Description { get; } 23 | public string? DefaultValue { get; } 24 | public string ValueTypeName { get; } 25 | public int? Index { get; } 26 | public bool IsRequired => DefaultValue == null && !IsParams; 27 | public bool IsFlag { get; } 28 | public bool IsParams { get; } 29 | public bool IsHidden { get; } 30 | public bool IsDefaultValueHidden { get; } 31 | public string FormattedValueTypeName => "<" + ValueTypeName + ">"; 32 | 33 | public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden, bool isDefaultValueHidden) 34 | { 35 | Options = options; 36 | Description = description; 37 | ValueTypeName = valueTypeName; 38 | DefaultValue = defaultValue; 39 | Index = index; 40 | IsFlag = isFlag; 41 | IsParams = isParams; 42 | IsHidden = isHidden; 43 | IsDefaultValueHidden = isDefaultValueHidden; 44 | } 45 | } 46 | 47 | [JsonSerializable(typeof(CommandHelpDefinition))] 48 | [JsonSerializable(typeof(CommandOptionHelpDefinition))] 49 | [JsonSerializable(typeof(CommandHelpDefinition[]))] 50 | [JsonSerializable(typeof(CommandOptionHelpDefinition[]))] 51 | [JsonSerializable(typeof(string[]))] 52 | [JsonSerializable(typeof(string))] 53 | [JsonSerializable(typeof(int))] 54 | [JsonSerializable(typeof(bool))] 55 | public partial class CliSchemaJsonSerializerContext : JsonSerializerContext { } 56 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/GeneratorSandbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | preview 7 | 8 | enable 9 | disable 10 | true 11 | 1701;1702;CS8321 12 | 13 | USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS 14 | 15 | Debug;Release 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Analyzer 40 | false 41 | 42 | 43 | 44 | 45 | 46 | 47 | PreserveNewest 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/ArrayParseTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class ArrayParseTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task Params() 9 | { 10 | var code = """ 11 | ConsoleApp.Run(args, (params int[] foo) => 12 | { 13 | Console.Write("[" + string.Join(", ", foo) + "]"); 14 | }); 15 | """; 16 | await verifier.Execute(code, args: "--foo", expected: "[]"); 17 | await verifier.Execute(code, args: "--foo 10", expected: "[10]"); 18 | await verifier.Execute(code, args: "--foo 10 20 30", expected: "[10, 20, 30]"); 19 | } 20 | 21 | [Test] 22 | public async Task ArgumentParams() 23 | { 24 | var code = """ 25 | ConsoleApp.Run(args, ([Argument]string title, [Argument]params int[] foo) => 26 | { 27 | Console.Write(title + "[" + string.Join(", ", foo) + "]"); 28 | }); 29 | """; 30 | await verifier.Execute(code, args: "aiueo", expected: "aiueo[]"); 31 | await verifier.Execute(code, args: "aiueo 10", expected: "aiueo[10]"); 32 | await verifier.Execute(code, args: "aiueo 10 20 30", expected: "aiueo[10, 20, 30]"); 33 | } 34 | 35 | [Test] 36 | public async Task ParseArray() 37 | { 38 | var code = """ 39 | ConsoleApp.Run(args, (int[] ix, string[] sx) => 40 | { 41 | Console.Write("[" + string.Join(", ", ix) + "]"); 42 | Console.Write("[" + string.Join(", ", sx) + "]"); 43 | }); 44 | """; 45 | await verifier.Execute(code, args: "--ix 1,2,3,4,5 --sx a,b,c,d,e", expected: "[1, 2, 3, 4, 5][a, b, c, d, e]"); 46 | 47 | var largeIntArray = string.Join(",", Enumerable.Range(0, 1000)); 48 | var expectedIntArray = string.Join(", ", Enumerable.Range(0, 1000)); 49 | await verifier.Execute(code, args: $"--ix {largeIntArray} --sx a,b,c,d,e", expected: $"[{expectedIntArray}][a, b, c, d, e]"); 50 | } 51 | 52 | [Test] 53 | public async Task JsonArray() 54 | { 55 | var code = """ 56 | ConsoleApp.Run(args, (int[] ix, string[] sx) => 57 | { 58 | Console.Write("[" + string.Join(", ", ix) + "]"); 59 | Console.Write("[" + string.Join(", ", sx) + "]"); 60 | }); 61 | """; 62 | await verifier.Execute(code, args: "--ix [] --sx []", expected: "[][]"); 63 | await verifier.Execute(code, args: "--ix [1,2,3,4,5] --sx [\"a\",\"b\",\"c\",\"d\",\"e\"]", expected: "[1, 2, 3, 4, 5][a, b, c, d, e]"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/EquatableTypeSymbol.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | 4 | namespace ConsoleAppFramework; 5 | 6 | public class EquatableTypeSymbol(ITypeSymbol typeSymbol) : IEquatable 7 | { 8 | // Used for build argument parser, maybe ok to equals name. 9 | public ITypeSymbol TypeSymbol => typeSymbol; 10 | 11 | // GetMembers is called for Enum and fields is not condition for command equality. 12 | public ImmutableArray GetMembers() => typeSymbol.GetMembers(); 13 | 14 | public TypeKind TypeKind { get; } = typeSymbol.TypeKind; 15 | public SpecialType SpecialType { get; } = typeSymbol.SpecialType; 16 | 17 | public string ToFullyQualifiedFormatDisplayString() => typeSymbol.ToFullyQualifiedFormatDisplayString(); 18 | public string ToDisplayString(NullableFlowState state, SymbolDisplayFormat format) => typeSymbol.ToDisplayString(state, format); 19 | 20 | public bool Equals(EquatableTypeSymbol other) 21 | { 22 | if (this.TypeKind != other.TypeKind) return false; 23 | if (this.SpecialType != other.SpecialType) return false; 24 | if (this.TypeSymbol.Name != other.TypeSymbol.Name) return false; 25 | 26 | return this.TypeSymbol.EqualsNamespaceAndName(other.TypeSymbol); 27 | } 28 | } 29 | 30 | // for filter 31 | public class EquatableTypeSymbolWithKeyedServiceKey 32 | : EquatableTypeSymbol, IEquatable 33 | { 34 | public bool IsKeyedService { get; } 35 | public string? FormattedKeyedServiceKey { get; } 36 | 37 | public EquatableTypeSymbolWithKeyedServiceKey(IParameterSymbol symbol) 38 | : base(symbol.Type) 39 | { 40 | var keyedServciesAttr = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "FromKeyedServicesAttribute"); 41 | if (keyedServciesAttr != null) 42 | { 43 | this.IsKeyedService = true; 44 | this.FormattedKeyedServiceKey = CommandParameter.GetFormattedKeyedServiceKey(keyedServciesAttr.ConstructorArguments[0].Value); 45 | } 46 | } 47 | 48 | public bool Equals(EquatableTypeSymbolWithKeyedServiceKey other) 49 | { 50 | if (base.Equals(other)) 51 | { 52 | if (IsKeyedService != other.IsKeyedService) return false; 53 | if (FormattedKeyedServiceKey != other.FormattedKeyedServiceKey) return false; 54 | return true; 55 | } 56 | 57 | return false; 58 | } 59 | } 60 | 61 | static class EquatableTypeSymbolExtensions 62 | { 63 | public static EquatableTypeSymbol ToEquatable(this ITypeSymbol typeSymbol) => new(typeSymbol); 64 | } 65 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class NameConverterTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task KebabCase() 9 | { 10 | await Assert.That(NameConverter.ToKebabCase("")).IsEqualTo(""); 11 | await Assert.That(NameConverter.ToKebabCase("HelloWorld")).IsEqualTo("hello-world"); 12 | await Assert.That(NameConverter.ToKebabCase("HelloWorldMyHome")).IsEqualTo("hello-world-my-home"); 13 | await Assert.That(NameConverter.ToKebabCase("helloWorld")).IsEqualTo("hello-world"); 14 | await Assert.That(NameConverter.ToKebabCase("hello-world")).IsEqualTo("hello-world"); 15 | await Assert.That(NameConverter.ToKebabCase("A")).IsEqualTo("a"); 16 | await Assert.That(NameConverter.ToKebabCase("AB")).IsEqualTo("ab"); 17 | await Assert.That(NameConverter.ToKebabCase("ABC")).IsEqualTo("abc"); 18 | await Assert.That(NameConverter.ToKebabCase("ABCD")).IsEqualTo("abcd"); 19 | await Assert.That(NameConverter.ToKebabCase("ABCDeF")).IsEqualTo("abc-def"); 20 | await Assert.That(NameConverter.ToKebabCase("XmlReader")).IsEqualTo("xml-reader"); 21 | await Assert.That(NameConverter.ToKebabCase("XMLReader")).IsEqualTo("xml-reader"); 22 | await Assert.That(NameConverter.ToKebabCase("MLLibrary")).IsEqualTo("ml-library"); 23 | } 24 | 25 | [Test] 26 | public async Task CommandName() 27 | { 28 | await verifier.Execute(""" 29 | var builder = ConsoleApp.Create(); 30 | builder.Add(); 31 | builder.Run(args); 32 | 33 | public class MyClass 34 | { 35 | public async Task HelloWorld() 36 | { 37 | Console.Write("Hello World!"); 38 | } 39 | } 40 | """, args: "hello-world", expected: "Hello World!"); 41 | } 42 | 43 | [Test] 44 | public async Task OptionName() 45 | { 46 | await verifier.Execute(""" 47 | var builder = ConsoleApp.Create(); 48 | builder.Add(); 49 | builder.Run(args); 50 | 51 | public class MyClass 52 | { 53 | public async Task HelloWorld(string fooBar) 54 | { 55 | Console.Write("Hello World! " + fooBar); 56 | } 57 | } 58 | """, args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo"); 59 | 60 | 61 | await verifier.Execute(""" 62 | var builder = ConsoleApp.Create(); 63 | var mc = new MyClass(); 64 | builder.Add("hello-world", mc.HelloWorld); 65 | builder.Run(args); 66 | 67 | public class MyClass 68 | { 69 | public async Task HelloWorld(string fooBar) 70 | { 71 | Console.Write("Hello World! " + fooBar); 72 | } 73 | } 74 | """, args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/WellKnownTypes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | public class WellKnownTypes(Compilation compilation) 6 | { 7 | INamedTypeSymbol? dateTimeOffset; 8 | public INamedTypeSymbol DateTimeOffset => dateTimeOffset ??= GetTypeByMetadataName("System.DateTimeOffset"); 9 | 10 | INamedTypeSymbol? guid; 11 | public INamedTypeSymbol Guid => guid ??= GetTypeByMetadataName("System.Guid"); 12 | 13 | INamedTypeSymbol? version; 14 | public INamedTypeSymbol Version => version ??= GetTypeByMetadataName("System.Version"); 15 | 16 | INamedTypeSymbol? dateTime; 17 | public INamedTypeSymbol DateTime => dateTime ??= GetTypeByMetadataName("System.DateTime"); 18 | 19 | INamedTypeSymbol? timeOnly; 20 | public INamedTypeSymbol TimeOnly => timeOnly ??= GetTypeByMetadataName("System.TimeOnly"); 21 | 22 | INamedTypeSymbol? dateOnly; 23 | public INamedTypeSymbol DateOnly => dateOnly ??= GetTypeByMetadataName("System.DateOnly"); 24 | 25 | INamedTypeSymbol? spanParsable; 26 | public INamedTypeSymbol? ISpanParsable => spanParsable ??= compilation.GetTypeByMetadataName("System.ISpanParsable`1"); 27 | 28 | INamedTypeSymbol? cancellationToken; 29 | public INamedTypeSymbol CancellationToken => cancellationToken ??= GetTypeByMetadataName("System.Threading.CancellationToken"); 30 | 31 | INamedTypeSymbol? task; 32 | public INamedTypeSymbol Task => task ??= GetTypeByMetadataName("System.Threading.Tasks.Task"); 33 | 34 | INamedTypeSymbol? task_T; 35 | public INamedTypeSymbol Task_T => task_T ??= GetTypeByMetadataName("System.Threading.Tasks.Task`1"); 36 | 37 | INamedTypeSymbol? disposable; 38 | public INamedTypeSymbol IDisposable => disposable ??= GetTypeByMetadataName("System.IDisposable"); 39 | 40 | INamedTypeSymbol? asyncDisposable; 41 | public INamedTypeSymbol IAsyncDisposable => asyncDisposable ??= GetTypeByMetadataName("System.IAsyncDisposable"); 42 | 43 | public bool HasTryParse(ITypeSymbol type) 44 | { 45 | if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset) 46 | || SymbolEqualityComparer.Default.Equals(type, Guid) 47 | || SymbolEqualityComparer.Default.Equals(type, DateTime) 48 | || SymbolEqualityComparer.Default.Equals(type, DateOnly) 49 | || SymbolEqualityComparer.Default.Equals(type, TimeOnly) 50 | || SymbolEqualityComparer.Default.Equals(type, Version) 51 | ) 52 | { 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | INamedTypeSymbol GetTypeByMetadataName(string metadataName) 59 | { 60 | var symbol = compilation.GetTypeByMetadataName(metadataName); 61 | if (symbol == null) 62 | { 63 | throw new InvalidOperationException($"Type {metadataName} is not found in compilation."); 64 | } 65 | return symbol; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/DITest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class DITest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task ServiceProvider() 9 | { 10 | await verifier.Execute(""" 11 | #nullable enable 12 | 13 | var di = new MiniDI(); 14 | di.Register(typeof(MyClass), new MyClass("foo")); 15 | ConsoleApp.ServiceProvider = di; 16 | 17 | ConsoleApp.Run(args, ([FromServices] MyClass mc, int x, int y) => { Console.Write(mc.Name + ":" + x + ":" + y); }); 18 | 19 | 20 | class MiniDI : IServiceProvider 21 | { 22 | System.Collections.Generic.Dictionary dict = new(); 23 | 24 | public void Register(Type type, object instance) 25 | { 26 | dict[type] = instance; 27 | } 28 | 29 | public object? GetService(Type serviceType) 30 | { 31 | return dict.TryGetValue(serviceType, out var instance) ? instance : null; 32 | } 33 | } 34 | 35 | class MyClass(string name) 36 | { 37 | public string Name => name; 38 | } 39 | """, args: "--x 10 --y 20", expected: "foo:10:20"); 40 | } 41 | 42 | [Test] 43 | public async Task WithFilter() 44 | { 45 | await verifier.Execute(""" 46 | var app = ConsoleApp.Create(); 47 | app.UseFilter(); 48 | app.Run(["cmd", "test"]); 49 | 50 | public class MyService 51 | { 52 | public void Test() => Console.Write("Test"); 53 | } 54 | 55 | internal class MyFilter(ConsoleAppFilter next, MyService myService) : ConsoleAppFilter(next) 56 | { 57 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 58 | { 59 | myService.Test(); 60 | await Next.InvokeAsync(context, cancellationToken); 61 | } 62 | } 63 | 64 | [RegisterCommands("cmd")] 65 | public class MyCommand 66 | { 67 | [Command("test")] 68 | public int Test() 69 | { 70 | return 1; 71 | } 72 | } 73 | 74 | class MiniDI : IServiceProvider 75 | { 76 | System.Collections.Generic.Dictionary dict = new(); 77 | 78 | public void Register(Type type, object instance) 79 | { 80 | dict[type] = instance; 81 | } 82 | 83 | public object? GetService(Type serviceType) 84 | { 85 | return dict.TryGetValue(serviceType, out var instance) ? instance : null; 86 | } 87 | } 88 | 89 | namespace ConsoleAppFramework 90 | { 91 | partial class ConsoleApp 92 | { 93 | partial class ConsoleAppBuilder 94 | { 95 | partial void BuildAndSetServiceProvider(ConsoleAppContext context) 96 | { 97 | var di = new MiniDI(); 98 | di.Register(typeof(MyService), new MyService()); 99 | ConsoleApp.ServiceProvider = di; 100 | } 101 | } 102 | } 103 | } 104 | """, "cmd test", "Test"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/SourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | // indent-level: 0. using namespace, class, struct declaration 6 | // indent-level: 1. field, property, method declaration 7 | // indent-level: 2. method body 8 | internal class SourceBuilder(int level) 9 | { 10 | StringBuilder builder = new StringBuilder(); 11 | 12 | public void Indent(int levelIncr = 1) 13 | { 14 | level += levelIncr; 15 | } 16 | 17 | public void Unindent(int levelDecr = 1) 18 | { 19 | level -= levelDecr; 20 | } 21 | 22 | public Scope BeginIndent() 23 | { 24 | Indent(); 25 | return new Scope(this); 26 | } 27 | 28 | public Scope BeginIndent(string code) 29 | { 30 | AppendLine(code); 31 | Indent(); 32 | return new Scope(this); 33 | } 34 | 35 | public Block BeginBlock() 36 | { 37 | AppendLine("{"); 38 | Indent(); 39 | return new Block(this); 40 | } 41 | 42 | public Block BeginBlock(string code) 43 | { 44 | AppendLine(code); 45 | AppendLine("{"); 46 | Indent(); 47 | return new Block(this); 48 | } 49 | 50 | public IDisposable Nop => NullDisposable.Instance; 51 | 52 | public void AppendLine() 53 | { 54 | builder.AppendLine(); 55 | } 56 | 57 | public void AppendLineIfExists(ReadOnlySpan values) 58 | { 59 | if (values.Length != 0) 60 | { 61 | builder.AppendLine(); 62 | } 63 | } 64 | 65 | public void AppendLine(string text) 66 | { 67 | if (level != 0) 68 | { 69 | builder.Append(' ', level * 4); // four spaces 70 | } 71 | builder.AppendLine(text); 72 | } 73 | 74 | public void AppendWithoutIndent(string text) 75 | { 76 | builder.Append(text); 77 | } 78 | 79 | public void AppendLineWithoutIndent(string text) 80 | { 81 | builder.AppendLine(text); 82 | } 83 | 84 | public override string ToString() => builder.ToString(); 85 | 86 | public struct Scope(SourceBuilder parent) : IDisposable 87 | { 88 | public void Dispose() 89 | { 90 | parent.Unindent(); 91 | } 92 | } 93 | 94 | public struct Block(SourceBuilder parent) : IDisposable 95 | { 96 | public void Dispose() 97 | { 98 | parent.Unindent(); 99 | parent.AppendLine("}"); 100 | } 101 | } 102 | 103 | public SourceBuilder Clone() 104 | { 105 | var sb = new SourceBuilder(level); 106 | sb.builder.Append(builder.ToString()); 107 | return sb; 108 | } 109 | 110 | class NullDisposable : IDisposable 111 | { 112 | public static readonly IDisposable Instance = new NullDisposable(); 113 | 114 | public void Dispose() 115 | { 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.NativeAotTests/NativeAotTest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | // This test verifies the behavior of NativeAOT. 4 | // Instead of running dotnet test, you must execute the exe generated by dotnet publish. 5 | // In the GitHub Actions workflow: 6 | // - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj 7 | // - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests 8 | 9 | // not parallel to check exit-code 10 | [assembly: NotInParallel] 11 | 12 | namespace ConsoleAppFramework.NativeAotTests; 13 | 14 | public class NativeAotTest 15 | { 16 | ConsoleApp.ConsoleAppBuilder app; 17 | 18 | public NativeAotTest() 19 | { 20 | Environment.ExitCode = 0; // reset ExitCode 21 | 22 | this.app = ConsoleApp.Create(); 23 | 24 | app.UseFilter(); 25 | app.Add("", Commands.Root); 26 | app.Add("json", Commands.RecordJson); 27 | } 28 | 29 | [Test] 30 | public async Task RunWithFilter() 31 | { 32 | // check NativeAot trimming,command requires [DynamicDependency] 33 | string[] runArgs = 34 | [ 35 | "input.txt", 36 | "--count", "3", 37 | "--quiet" 38 | ]; 39 | 40 | await app.RunAsync(runArgs); 41 | await Assert.That(Environment.ExitCode).IsEqualTo(0); 42 | } 43 | 44 | [Test] 45 | public async Task JsonInvalid() 46 | { 47 | string[] runArgs = 48 | [ 49 | "json", 50 | "--record", 51 | "{ \"X\" = 10, \"Y\" = 20 }" 52 | ]; 53 | 54 | await app.RunAsync(runArgs); 55 | await Assert.That(Environment.ExitCode).IsNotEqualTo(0); 56 | } 57 | } 58 | 59 | internal static class Commands 60 | { 61 | public static async Task Root( 62 | [Argument] string path, 63 | [Range(1, 10)] int count = 1, 64 | bool quiet = false, 65 | CancellationToken cancellationToken = default) 66 | { 67 | await Task.Delay(10, cancellationToken); 68 | if (!quiet) 69 | { 70 | Console.WriteLine($"Processing {path} with count {count}"); 71 | } 72 | return 0; 73 | } 74 | 75 | public static void RecordJson(MyRecord record) 76 | { 77 | Console.WriteLine($"Record: X={record.X}, Y={record.Y}"); 78 | } 79 | } 80 | 81 | 82 | internal sealed class LoggingFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 83 | { 84 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 85 | { 86 | try 87 | { 88 | await Next.InvokeAsync(context, cancellationToken); 89 | } 90 | catch (Exception ex) 91 | { 92 | Console.WriteLine($"Unhandled exception: {ex.Message}"); 93 | throw; 94 | } 95 | } 96 | } 97 | 98 | public record MyRecord(int X, int Y); 99 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/ArgumentParserTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | 4 | namespace ConsoleAppFramework.GeneratorTests; 5 | 6 | public class ArgumentParserTest 7 | { 8 | readonly VerifyHelper verifier = new("CAF"); 9 | 10 | [Test] 11 | public async Task Lamda() 12 | { 13 | await verifier.Execute(HEAD + Body(""" 14 | ConsoleApp.Run(args, ([Vector3Parser] Vector3 v) => Console.Write(v)); 15 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>"); 16 | await verifier.Execute(HEAD + Body(""" 17 | var app = ConsoleApp.Create(); 18 | app.Add("", ([Vector3Parser] Vector3 v) => Console.Write(v)); 19 | app.Run(args); 20 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>"); 21 | } 22 | 23 | [Test] 24 | public async Task Method() 25 | { 26 | await verifier.Execute(HEAD + Body(""" 27 | ConsoleApp.Run(args, MyCommands.Static); 28 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>"); 29 | await verifier.Execute(HEAD + Body(""" 30 | var app = ConsoleApp.Create(); 31 | app.Add("", MyCommands.Static); 32 | app.Run(args); 33 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>"); 34 | } 35 | 36 | [Test] 37 | public async Task Class() 38 | { 39 | await verifier.Execute(HEAD + Body(""" 40 | var app = ConsoleApp.Create(); 41 | app.Add(); 42 | app.Run(args); 43 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>"); 44 | } 45 | 46 | static string Body([StringSyntax("C#-test")] string code) => code; 47 | /// 48 | /// 49 | /// 50 | [StringSyntax("C#-test")] 51 | const string 52 | HEAD = """ 53 | using System.Numerics; 54 | 55 | """, 56 | TAIL = """ 57 | 58 | public class MyCommands 59 | { 60 | [Command("")] 61 | public void Root([Vector3Parser] Vector3 v) => Console.Write(v); 62 | public static void Static([Vector3Parser] Vector3 v) => Console.Write(v); 63 | } 64 | 65 | [AttributeUsage(AttributeTargets.Parameter)] 66 | public class Vector3ParserAttribute : Attribute, IArgumentParser 67 | { 68 | public static bool TryParse(ReadOnlySpan s, out Vector3 result) 69 | { 70 | Span ranges = stackalloc Range[3]; 71 | var splitCount = s.Split(ranges, ','); 72 | if (splitCount != 3) 73 | { 74 | result = default; 75 | return false; 76 | } 77 | 78 | float x; 79 | float y; 80 | float z; 81 | if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z)) 82 | { 83 | result = new Vector3(x, y, z); 84 | return true; 85 | } 86 | 87 | result = default; 88 | return false; 89 | } 90 | } 91 | """; 92 | } 93 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/BuildCustomDelegateTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleAppFramework.GeneratorTests; 8 | 9 | public class BuildCustomDelegateTest 10 | { 11 | VerifyHelper verifier = new VerifyHelper("CAF"); 12 | 13 | [Test] 14 | public async Task Run() 15 | { 16 | var code = """ 17 | ConsoleApp.Run(args, ( 18 | bool a1, 19 | bool a2, 20 | bool a3, 21 | bool a4, 22 | bool a5, 23 | bool a6, 24 | bool a7, 25 | bool a8, 26 | bool a9, 27 | bool a10, 28 | bool a11, 29 | bool a12, 30 | bool a13, 31 | bool a14, 32 | bool a15, 33 | bool a16 // ok it is Action 34 | ) => { Console.Write("ok"); }); 35 | """; 36 | 37 | await verifier.Execute(code, "", "ok"); 38 | 39 | var code2 = """ 40 | ConsoleApp.Run(args, ( 41 | bool a1, 42 | bool a2, 43 | bool a3, 44 | bool a4, 45 | bool a5, 46 | bool a6, 47 | bool a7, 48 | bool a8, 49 | bool a9, 50 | bool a10, 51 | bool a11, 52 | bool a12, 53 | bool a13, 54 | bool a14, 55 | bool a15, 56 | bool a16, 57 | bool a17 // custom delegate 58 | ) => { Console.Write("ok"); }); 59 | """; 60 | 61 | await verifier.Execute(code2, "", "ok"); 62 | 63 | 64 | await verifier.Execute(""" 65 | var t = new Test(); 66 | ConsoleApp.Run(args, t.Handle); 67 | 68 | public partial class Test 69 | { 70 | public void Handle( 71 | bool a1, 72 | bool a2, 73 | bool a3, 74 | bool a4, 75 | bool a5, 76 | bool a6, 77 | bool a7, 78 | bool a8, 79 | bool a9, 80 | bool a10, 81 | bool a11, 82 | bool a12, 83 | bool a13, 84 | bool a14, 85 | bool a15, 86 | bool a16, 87 | bool a17, 88 | bool a18, 89 | bool a19 90 | ) 91 | { 92 | Console.Write("ok"); 93 | } 94 | } 95 | """, "", "ok"); 96 | 97 | 98 | 99 | await verifier.Execute(""" 100 | unsafe 101 | { 102 | ConsoleApp.Run(args, &Test.Handle); 103 | } 104 | 105 | public partial class Test 106 | { 107 | public static void Handle( 108 | bool a1, 109 | bool a2, 110 | bool a3, 111 | bool a4, 112 | bool a5, 113 | bool a6, 114 | bool a7, 115 | bool a8, 116 | bool a9, 117 | bool a10, 118 | bool a11, 119 | bool a12, 120 | bool a13, 121 | bool a14, 122 | bool a15, 123 | bool a16, 124 | bool a17, 125 | bool a18, 126 | bool a19 127 | ) 128 | { 129 | Console.Write("ok"); 130 | } 131 | } 132 | """, "", "ok"); 133 | } 134 | 135 | [Test] 136 | public async Task Builder() 137 | { 138 | await verifier.Execute(""" 139 | var t = new Test(); 140 | 141 | var app = ConsoleApp.Create(); 142 | app.Add("", t.Handle); 143 | app.Run(args); 144 | 145 | public partial class Test 146 | { 147 | public void Handle( 148 | bool a1, 149 | bool a2, 150 | bool a3, 151 | bool a4, 152 | bool a5, 153 | bool a6, 154 | bool a7, 155 | bool a8, 156 | bool a9, 157 | bool a10, 158 | bool a11, 159 | bool a12, 160 | bool a13, 161 | bool a14, 162 | bool a15, 163 | bool a16, 164 | bool a17, 165 | bool a18, 166 | bool a19 167 | ) 168 | { 169 | Console.Write("ok"); 170 | } 171 | } 172 | """, "", "ok"); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/Temp.cs: -------------------------------------------------------------------------------- 1 | //// 2 | //#nullable enable 3 | //#pragma warning disable CS0108 // hides inherited member 4 | //#pragma warning disable CS0162 // Unreachable code 5 | //#pragma warning disable CS0164 // This label has not been referenced 6 | //#pragma warning disable CS0219 // Variable assigned but never used 7 | //#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. 8 | //#pragma warning disable CS8601 // Possible null reference assignment 9 | //#pragma warning disable CS8602 10 | //#pragma warning disable CS8604 // Possible null reference argument for parameter 11 | //#pragma warning disable CS8619 12 | //#pragma warning disable CS8620 13 | //#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method 14 | //#pragma warning disable CS8765 // Nullability of type of parameter 15 | //#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member 16 | //#pragma warning disable CA1050 // Declare types in namespaces. 17 | //#pragma warning disable CS1998 18 | //#pragma warning disable CS8625 19 | 20 | //namespace ConsoleAppFramework; 21 | 22 | //using System; 23 | //using System.Text; 24 | //using System.Reflection; 25 | //using System.Threading; 26 | //using System.Threading.Tasks; 27 | //using System.Runtime.InteropServices; 28 | //using System.Runtime.CompilerServices; 29 | //using System.Diagnostics.CodeAnalysis; 30 | //using System.ComponentModel.DataAnnotations; 31 | 32 | //using Microsoft.Extensions.DependencyInjection; 33 | //using Microsoft.Extensions.Logging; 34 | //using Microsoft.Extensions.Configuration; 35 | //using Microsoft.Extensions.Hosting; 36 | 37 | //internal static class ConsoleAppHostBuilderExtensions 38 | //{ 39 | // class CompositeDisposableServiceProvider(IDisposable host, IServiceProvider serviceServiceProvider, IDisposable scope, IServiceProvider serviceProvider) 40 | // : IServiceProvider, IDisposable 41 | // { 42 | // public object? GetService(Type serviceType) 43 | // { 44 | // return serviceProvider.GetService(serviceType); 45 | // } 46 | 47 | // public void Dispose() 48 | // { 49 | // if (serviceProvider is IDisposable d) 50 | // { 51 | // d.Dispose(); 52 | // } 53 | // scope.Dispose(); 54 | // if (serviceServiceProvider is IDisposable d2) 55 | // { 56 | // d2.Dispose(); 57 | // } 58 | // host.Dispose(); 59 | // } 60 | // } 61 | 62 | // internal static ConsoleApp.ConsoleAppBuilder ToConsoleAppBuilder(this IHostBuilder hostBuilder) 63 | // { 64 | // var host = hostBuilder.Build(); 65 | // var serviceServiceProvider = host.Services; 66 | // var scope = serviceServiceProvider.CreateScope(); 67 | // var serviceProvider = scope.ServiceProvider; 68 | // ConsoleApp.ServiceProvider = new CompositeDisposableServiceProvider(host, serviceServiceProvider, scope, serviceProvider); 69 | 70 | // return ConsoleApp.Create(); 71 | // } 72 | 73 | // internal static ConsoleApp.ConsoleAppBuilder ToConsoleAppBuilder(this HostApplicationBuilder hostBuilder) 74 | // { 75 | // var host = hostBuilder.Build(); 76 | // var serviceServiceProvider = host.Services; 77 | // var scope = serviceServiceProvider.CreateScope(); 78 | // var serviceProvider = scope.ServiceProvider; 79 | // ConsoleApp.ServiceProvider = new CompositeDisposableServiceProvider(host, serviceServiceProvider, scope, serviceProvider); 80 | 81 | // return ConsoleApp.Create(); 82 | // } 83 | //} 84 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class ConsoleAppContextTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task ForLambda() 9 | { 10 | await verifier.Execute(""" 11 | ConsoleApp.Run(args, (ConsoleAppContext ctx) => { Console.Write(ctx.Arguments.Length); }); 12 | """, args: "", expected: "0"); 13 | } 14 | 15 | [Test] 16 | public async Task ForMethod() 17 | { 18 | await verifier.Execute(""" 19 | var builder = ConsoleApp.Create(); 20 | 21 | builder.UseFilter(); 22 | 23 | builder.Add("", Hello); 24 | 25 | builder.Run(args); 26 | 27 | void Hello(ConsoleAppContext ctx) 28 | { 29 | Console.Write(ctx.State); 30 | } 31 | 32 | internal class StateFilter(ConsoleAppFilter next) 33 | : ConsoleAppFilter(next) 34 | { 35 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 36 | { 37 | Console.Write(1); 38 | return Next.InvokeAsync(context with { State = 2 }, cancellationToken); 39 | } 40 | } 41 | """, args: "", expected: "12"); 42 | } 43 | 44 | [Test] 45 | [Arguments("--x 1 --y 2", "", "--x 1 --y 2", "")] // no command, no espace 46 | [Arguments("foo --x 1 --y 2", "foo", "--x 1 --y 2", "")] // command, no espace 47 | [Arguments("foo bar --x 1 --y 2", "foo bar", "--x 1 --y 2", "")] // nested command, no espace 48 | [Arguments("--x 1 --y 2 -- abc", "", "--x 1 --y 2", "abc")] // no command, espace 49 | [Arguments("--x 1 --y 2 -- abc def", "", "--x 1 --y 2", "abc def")] // no command, espace2 50 | [Arguments("foo --x 1 --y 2 -- abc", "foo", "--x 1 --y 2", "abc")] // command, espace 51 | [Arguments("foo --x 1 --y 2 -- abc def", "foo", "--x 1 --y 2", "abc def")] // command, espace2 52 | [Arguments("foo bar --x 1 --y 2 -- abc", "foo bar", "--x 1 --y 2", "abc")] // nested command, espace 53 | [Arguments("foo bar --x 1 --y 2 -- abc def", "foo bar", "--x 1 --y 2", "abc def")] // nested command, espace2 54 | public async Task ArgumentsParseTest(string args, string commandName, string expectedCommandArguments, string expectedEscapedArguments) 55 | { 56 | var argsSpan = args.Split(' ').AsSpan(); 57 | var commandDepth = (commandName == "") ? 0 : (argsSpan.Length - args.Replace(commandName, "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Length); 58 | var escapeIndex = argsSpan.IndexOf("--"); 59 | 60 | var ctx = new ConsoleAppContext2(commandName, argsSpan.ToArray(), null, commandDepth, escapeIndex); 61 | 62 | await Assert.That(string.Join(" ", ctx.CommandArguments!)).IsEqualTo(expectedCommandArguments); 63 | await Assert.That(string.Join(" ", ctx.EscapedArguments!)).IsEqualTo(expectedEscapedArguments); 64 | } 65 | 66 | public class ConsoleAppContext2 67 | { 68 | public string CommandName { get; } 69 | public string[] Arguments { get; } 70 | public object? State { get; } 71 | 72 | int commandDepth; 73 | int escapeIndex; 74 | 75 | public ReadOnlySpan CommandArguments 76 | { 77 | get => (escapeIndex == -1) 78 | ? Arguments.AsSpan(commandDepth) 79 | : Arguments.AsSpan(commandDepth, escapeIndex - commandDepth); 80 | } 81 | 82 | public ReadOnlySpan EscapedArguments 83 | { 84 | get => (escapeIndex == -1) 85 | ? Array.Empty() 86 | : Arguments.AsSpan(escapeIndex + 1); 87 | } 88 | 89 | public ConsoleAppContext2(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex) 90 | { 91 | this.CommandName = commandName; 92 | this.Arguments = arguments; 93 | this.State = state; 94 | 95 | this.commandDepth = commandDepth; 96 | this.escapeIndex = escapeIndex; 97 | } 98 | 99 | public override string ToString() 100 | { 101 | return string.Join(" ", Arguments); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | public interface IArgumentParser 6 | { 7 | static abstract bool TryParse(ReadOnlySpan s, out T result); 8 | } 9 | 10 | /// 11 | /// Represents the execution context for a console application command, containing command metadata and parsed arguments. 12 | /// 13 | public record ConsoleAppContext 14 | { 15 | /// 16 | /// Gets the name of the command being executed. 17 | /// 18 | public string CommandName { get; init; } 19 | 20 | /// 21 | /// Gets the raw arguments passed to the application, including the command name itself. 22 | /// 23 | public string[] Arguments { get; init; } 24 | 25 | /// 26 | /// Gets the custom state object that can be used to share data across commands. 27 | /// 28 | public object? State { get; init; } 29 | 30 | /// 31 | /// Gets the parsed global options that apply across all commands. 32 | /// 33 | public object? GlobalOptions { get; init; } 34 | 35 | /// 36 | /// Gets the depth of the command in a nested command hierarchy. Used internally by the framework. 37 | /// 38 | [EditorBrowsable(EditorBrowsableState.Never)] 39 | public int CommandDepth { get; } 40 | 41 | /// 42 | /// Gets the index of the escape separator ('--') in the arguments, or -1 if not present. Used internally by the framework. 43 | /// 44 | [EditorBrowsable(EditorBrowsableState.Never)] 45 | public int EscapeIndex { get; } 46 | 47 | /// 48 | /// Gets the internal command arguments with global options removed. Used internally by the framework. 49 | /// 50 | [EditorBrowsable(EditorBrowsableState.Never)] 51 | public ReadOnlyMemory InternalCommandArgs { get; } 52 | 53 | /// 54 | /// Gets the arguments intended for the current command, excluding the command name and any escaped arguments after '--'. 55 | /// 56 | public ReadOnlySpan CommandArguments 57 | { 58 | get => (EscapeIndex == -1) 59 | ? Arguments.AsSpan(CommandDepth) 60 | : Arguments.AsSpan(CommandDepth, EscapeIndex - CommandDepth); 61 | } 62 | 63 | /// 64 | /// Gets the arguments that appear after the escape separator ('--'), which are not parsed by the command. 65 | /// Returns an empty span if no escape separator is present. 66 | /// 67 | public ReadOnlySpan EscapedArguments 68 | { 69 | get => (EscapeIndex == -1) 70 | ? Array.Empty() 71 | : Arguments.AsSpan(EscapeIndex + 1); 72 | } 73 | 74 | public ConsoleAppContext(string commandName, string[] arguments, ReadOnlyMemory internalCommandArgs, object? state, object? globalOptions, int commandDepth, int escapeIndex) 75 | { 76 | this.CommandName = commandName; 77 | this.Arguments = arguments; 78 | this.InternalCommandArgs = internalCommandArgs; 79 | this.State = state; 80 | this.GlobalOptions = globalOptions; 81 | this.CommandDepth = commandDepth; 82 | this.EscapeIndex = escapeIndex; 83 | } 84 | 85 | /// 86 | /// Returns a string representation of all arguments joined by spaces. 87 | /// 88 | /// A space-separated string of all arguments. 89 | public override string ToString() 90 | { 91 | return string.Join(" ", Arguments); 92 | } 93 | } 94 | 95 | public abstract class ConsoleAppFilter(ConsoleAppFilter next) 96 | { 97 | protected readonly ConsoleAppFilter Next = next; 98 | 99 | public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken); 100 | } 101 | 102 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] 103 | public sealed class ConsoleAppFilterAttribute : Attribute 104 | where T : ConsoleAppFilter 105 | { 106 | } 107 | 108 | public sealed class ArgumentParseFailedException(string message) : Exception(message) 109 | { 110 | } 111 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/HiddenAttributeTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class HiddenAtttributeTest 4 | { 5 | VerifyHelper verifier = new("CAF"); 6 | 7 | [Test] 8 | public async Task VerifyHiddenOptions_Lambda() 9 | { 10 | var code = 11 | """ 12 | ConsoleApp.Log = x => Console.WriteLine(x); 13 | ConsoleApp.Run(args, (int x, [Hidden]int y) => { }); 14 | """; 15 | 16 | // Verify Hidden options is not shown on command help. 17 | await verifier.Execute(code, args: "--help", expected: 18 | """ 19 | Usage: [options...] [-h|--help] [--version] 20 | 21 | Options: 22 | --x [Required] 23 | 24 | """); 25 | } 26 | 27 | [Test] 28 | public async Task VerifyHiddenCommands_Class() 29 | { 30 | var code = 31 | """ 32 | ConsoleApp.Log = x => Console.WriteLine(x); 33 | var builder = ConsoleApp.Create(); 34 | builder.Add(); 35 | await builder.RunAsync(args); 36 | 37 | public class Commands 38 | { 39 | [Hidden] 40 | public async Task Command1() { Console.Write("command1"); } 41 | 42 | public async Task Command2() { Console.Write("command2"); } 43 | 44 | [Hidden] 45 | public async Task Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); } 46 | } 47 | """; 48 | 49 | // Verify hidden command is not shown on root help commands. 50 | await verifier.Execute(code, args: "--help", expected: 51 | """ 52 | Usage: [command] [-h|--help] [--version] 53 | 54 | Commands: 55 | command2 56 | 57 | """); 58 | 59 | // Verify Hidden command help is shown when explicitly specify command name. 60 | await verifier.Execute(code, args: "command1 --help", expected: 61 | """ 62 | Usage: command1 [-h|--help] [--version] 63 | 64 | """); 65 | 66 | await verifier.Execute(code, args: "command2 --help", expected: 67 | """ 68 | Usage: command2 [-h|--help] [--version] 69 | 70 | """); 71 | 72 | await verifier.Execute(code, args: "command3 --help", expected: 73 | """ 74 | Usage: command3 [options...] [-h|--help] [--version] 75 | 76 | Options: 77 | --x [Required] 78 | 79 | """); 80 | 81 | // Verify commands involations 82 | await verifier.Execute(code, args: "command1", "command1"); 83 | await verifier.Execute(code, args: "command2", "command2"); 84 | await verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2"); 85 | } 86 | 87 | [Test] 88 | public async Task VerifyHiddenCommands_LocalFunctions() 89 | { 90 | var code = 91 | """ 92 | ConsoleApp.Log = x => Console.WriteLine(x); 93 | var builder = ConsoleApp.Create(); 94 | 95 | builder.Add("", () => { Console.Write("root"); }); 96 | builder.Add("command1", Command1); 97 | builder.Add("command2", Command2); 98 | builder.Add("command3", Command3); 99 | builder.Run(args); 100 | 101 | [Hidden] 102 | static void Command1() { Console.Write("command1"); } 103 | 104 | static void Command2() { Console.Write("command2"); } 105 | 106 | [Hidden] 107 | static void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); } 108 | """; 109 | 110 | await verifier.Execute(code, args: "--help", expected: 111 | """ 112 | Usage: [command] [-h|--help] [--version] 113 | 114 | Commands: 115 | command2 116 | 117 | """); 118 | 119 | // Verify commands can be invoked. 120 | await verifier.Execute(code, args: "command1", expected: "command1"); 121 | await verifier.Execute(code, args: "command2", expected: "command2"); 122 | await verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2"); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /sandbox/GeneratorSandbox/Filters.cs: -------------------------------------------------------------------------------- 1 | 2 | using ConsoleAppFramework; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | // using Microsoft.Extensions.DependencyInjection; 6 | using System.Diagnostics; 7 | using System.Reflection; 8 | 9 | namespace GeneratorSandbox; 10 | 11 | // ReadMe sample filters 12 | 13 | internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 14 | { 15 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 16 | { 17 | try 18 | { 19 | /* on before */ 20 | await Next.InvokeAsync(context, cancellationToken); // next 21 | /* on after */ 22 | } 23 | catch 24 | { 25 | /* on error */ 26 | throw; 27 | } 28 | finally 29 | { 30 | /* on finally */ 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | internal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 38 | { 39 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 40 | { 41 | var requestId = Guid.NewGuid(); 42 | var userId = await GetUserIdAsync(); 43 | 44 | // setup new state to context 45 | var authedContext = context with { State = new ApplicationContext(requestId, userId) }; 46 | await Next.InvokeAsync(authedContext, cancellationToken); 47 | } 48 | 49 | // get user-id from DB/auth saas/others 50 | async Task GetUserIdAsync() 51 | { 52 | await Task.Delay(TimeSpan.FromSeconds(1)); 53 | return 1999; 54 | } 55 | } 56 | 57 | record class ApplicationContext(Guid RequestId, int UserId); 58 | 59 | internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 60 | { 61 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 62 | { 63 | var startTime = Stopwatch.GetTimestamp(); 64 | ConsoleApp.Log($"Execute command at {DateTime.UtcNow.ToLocalTime()}"); // LocalTime for human readable time 65 | try 66 | { 67 | await Next.InvokeAsync(context, cancellationToken); 68 | ConsoleApp.Log($"Command execute successfully at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); 69 | } 70 | catch 71 | { 72 | ConsoleApp.Log($"Command execute failed at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); 73 | throw; 74 | } 75 | } 76 | } 77 | 78 | internal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 79 | { 80 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 81 | { 82 | try 83 | { 84 | await Next.InvokeAsync(context, cancellationToken); 85 | } 86 | catch (Exception ex) 87 | { 88 | if (ex is OperationCanceledException) return; 89 | 90 | Environment.ExitCode = 9999; // change custom exit code 91 | ConsoleApp.LogError(ex.ToString()); 92 | } 93 | } 94 | } 95 | 96 | internal class PreventMultipleSameCommandInvokeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 97 | { 98 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 99 | { 100 | var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_'); 101 | var mutexKey = $"{basePath}$$${context.CommandName}"; // lock per command-name 102 | 103 | using var mutex = new Mutex(true, mutexKey, out var createdNew); 104 | if (!createdNew) 105 | { 106 | throw new Exception($"already running command:{context.CommandName} in another process."); 107 | } 108 | 109 | await Next.InvokeAsync(context, cancellationToken); 110 | } 111 | } 112 | 113 | 114 | //internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next) 115 | //{ 116 | // public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 117 | // { 118 | // // create Microsoft.Extensions.DependencyInjection scope 119 | // await using var scope = serviceProvider.CreateAsyncScope(); 120 | // await Next.InvokeAsync(context, cancellationToken); 121 | // } 122 | //} 123 | -------------------------------------------------------------------------------- /sandbox/CliFrameworkBenchmark/Benchmark.cs: -------------------------------------------------------------------------------- 1 | // This benchmark project is based on CliFx.Benchmarks. 2 | // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Order; 6 | using CliFx; 7 | using CliFrameworkBenchmarks.Commands; 8 | using ConsoleAppFramework; 9 | using Spectre.Console.Cli; 10 | 11 | namespace CliFrameworkBenchmarks; 12 | 13 | [Orderer(SummaryOrderPolicy.FastestToSlowest)] 14 | public class Benchmark 15 | { 16 | private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; 17 | 18 | [Benchmark(Description = "Cocona.Lite")] 19 | public void ExecuteWithCoconaLite() 20 | { 21 | Cocona.CoconaLiteApp.Run(Arguments); 22 | } 23 | 24 | [Benchmark(Description = "Cocona")] 25 | public void ExecuteWithCocona() 26 | { 27 | Cocona.CoconaApp.Run(Arguments); 28 | } 29 | 30 | //[Benchmark(Description = "ConsoleAppFramework")] 31 | //public async ValueTask ExecuteWithConsoleAppFramework() => 32 | // await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); 33 | 34 | [Benchmark(Description = "CliFx")] 35 | public ValueTask ExecuteWithCliFx() 36 | { 37 | return new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); 38 | } 39 | 40 | [Benchmark(Description = "System.CommandLine v2")] 41 | public int ExecuteWithSystemCommandLine() 42 | { 43 | return SystemCommandLineCommand.ParseInvoke(Arguments); 44 | } 45 | 46 | [Benchmark(Description = "System.CommandLine v2(InvokeAsync)")] 47 | public Task ExecuteWithSystemCommandLineAsync() 48 | { 49 | return SystemCommandLineCommand.ParseInvokeAsync(Arguments); 50 | } 51 | 52 | //[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] 53 | //public int ExecuteWithMcMaster() => 54 | // McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); 55 | 56 | //[Benchmark(Description = "CommandLineParser")] 57 | //public void ExecuteWithCommandLineParser() => 58 | // new Parser() 59 | // .ParseArguments(Arguments, typeof(CommandLineParserCommand)) 60 | // .WithParsed(c => c.Execute()); 61 | 62 | //[Benchmark(Description = "PowerArgs")] 63 | //public void ExecuteWithPowerArgs() => 64 | // PowerArgs.Args.InvokeMain(Arguments); 65 | 66 | //[Benchmark(Description = "Clipr")] 67 | //public void ExecuteWithClipr() => 68 | // clipr.CliParser.Parse(Arguments).Execute(); 69 | 70 | 71 | //[Benchmark(Description = "ConsoleAppFramework v5")] 72 | //public void ExecuteConsoleAppFramework5() 73 | //{ 74 | // ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); 75 | //} 76 | 77 | [Benchmark(Description = "ConsoleAppFramework v5", Baseline = true)] 78 | public void ExecuteConsoleAppFramework() 79 | { 80 | ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); 81 | } 82 | 83 | [Benchmark(Description = "ConsoleAppFramework v5(app with CancellationToken)")] 84 | public Task ExecuteConsoleAppFramework2() 85 | { 86 | var app = ConsoleApp.Create(); 87 | app.Add("", ConsoleAppFrameworkCommand.ExecuteWithCancellationToken); 88 | return app.RunAsync(Arguments); 89 | } 90 | 91 | // for alpha testing 92 | //private static readonly string[] TempArguments = { "", "--str", "hello world", "-i", "13", "-b" }; 93 | //[Benchmark(Description = "ConsoleAppFramework.Builder")] 94 | //public unsafe void ExecuteConsoleAppFrameworkBuilder() 95 | //{ 96 | // var builder = ConsoleApp.Create(); 97 | // builder.Add("", ConsoleAppFrameworkCommand.Execute); 98 | // builder.Run(TempArguments); 99 | //} 100 | 101 | [Benchmark(Description = "Spectre.Console.Cli")] 102 | public void ExecuteSpectreConsoleCli() 103 | { 104 | var app = new CommandApp(); 105 | app.Run(Arguments); 106 | } 107 | 108 | 109 | //[Benchmark(Description = "ConsoleAppFramework Builder API")] 110 | //public unsafe void ExecuteConsoleAppFramework2() 111 | //{ 112 | // var app = ConsoleApp.Create(); 113 | // app.Add("", ConsoleAppFrameworkCommand.Execute); 114 | // app.Run(Arguments); 115 | //} 116 | 117 | //[Benchmark(Description = "ConsoleAppFramework CancellationToken")] 118 | //public unsafe void ExecuteConsoleAppFramework3() 119 | //{ 120 | // var app = ConsoleApp.Create(); 121 | // app.Add("", ConsoleAppFrameworkCommandWithCancellationToken.Execute); 122 | // app.Run(Arguments); 123 | //} 124 | 125 | //[Benchmark(Description = "ConsoleAppFramework With Filter")] 126 | //public unsafe void ExecuteConsoleAppFramework4() 127 | //{ 128 | // var app = ConsoleApp.Create(); 129 | // app.UseFilter(); 130 | // app.Add("", ConsoleAppFrameworkCommand.Execute); 131 | // app.Run(Arguments); 132 | //} 133 | } 134 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class SubCommandTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task Zeroargs() 9 | { 10 | var code = """ 11 | var builder = ConsoleApp.Create(); 12 | 13 | builder.Add("", () => { Console.Write("root"); }); 14 | builder.Add("a", () => { Console.Write("a"); }); 15 | builder.Add("a b1", () => { Console.Write("a b1"); }); 16 | builder.Add("a b2", () => { Console.Write("a b2"); }); 17 | builder.Add("a b2 c", () => { Console.Write("a b2 c"); }); 18 | builder.Add("a b2 d", () => { Console.Write("a b2 d"); }); 19 | builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); }); 20 | builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); }); 21 | 22 | builder.Run(args); 23 | """; 24 | 25 | await verifier.Execute(code, "", "root"); 26 | await verifier.Execute(code, "a", "a"); 27 | await verifier.Execute(code, "a b1", "a b1"); 28 | await verifier.Execute(code, "a b2", "a b2"); 29 | await verifier.Execute(code, "a b2 c", "a b2 c"); 30 | await verifier.Execute(code, "a b2 d", "a b2 d"); 31 | await verifier.Execute(code, "a b2 d e", "a b2 d e"); 32 | await verifier.Execute(code, "a b c d e f", "a b c d e f"); 33 | } 34 | 35 | [Test] 36 | public async Task Withargs() 37 | { 38 | var code = """ 39 | var builder = ConsoleApp.Create(); 40 | 41 | builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); 42 | builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); 43 | builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); }); 44 | builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); }); 45 | builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); }); 46 | builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); }); 47 | builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); }); 48 | builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); }); 49 | 50 | builder.Run(args); 51 | """; 52 | 53 | await verifier.Execute(code, "--x 10 --y 20", "root 10 20"); 54 | await verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); 55 | await verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); 56 | await verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); 57 | await verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20"); 58 | await verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20"); 59 | await verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20"); 60 | await verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20"); 61 | } 62 | 63 | [Test] 64 | public async Task ZeroargsAsync() 65 | { 66 | var code = """ 67 | var builder = ConsoleApp.Create(); 68 | 69 | builder.Add("", () => { Console.Write("root"); }); 70 | builder.Add("a", () => { Console.Write("a"); }); 71 | builder.Add("a b1", () => { Console.Write("a b1"); }); 72 | builder.Add("a b2", () => { Console.Write("a b2"); }); 73 | builder.Add("a b2 c", () => { Console.Write("a b2 c"); }); 74 | builder.Add("a b2 d", () => { Console.Write("a b2 d"); }); 75 | builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); }); 76 | builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); }); 77 | 78 | await builder.RunAsync(args); 79 | """; 80 | 81 | await verifier.Execute(code, "", "root"); 82 | await verifier.Execute(code, "a", "a"); 83 | await verifier.Execute(code, "a b1", "a b1"); 84 | await verifier.Execute(code, "a b2", "a b2"); 85 | await verifier.Execute(code, "a b2 c", "a b2 c"); 86 | await verifier.Execute(code, "a b2 d", "a b2 d"); 87 | await verifier.Execute(code, "a b2 d e", "a b2 d e"); 88 | await verifier.Execute(code, "a b c d e f", "a b c d e f"); 89 | } 90 | 91 | [Test] 92 | public async Task WithargsAsync() 93 | { 94 | var code = """ 95 | var builder = ConsoleApp.Create(); 96 | 97 | builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); 98 | builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); 99 | builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); }); 100 | builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); }); 101 | builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); }); 102 | builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); }); 103 | builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); }); 104 | builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); }); 105 | 106 | await builder.RunAsync(args); 107 | """; 108 | 109 | await verifier.Execute(code, "--x 10 --y 20", "root 10 20"); 110 | await verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); 111 | await verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); 112 | await verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); 113 | await verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20"); 114 | await verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20"); 115 | await verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20"); 116 | await verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20"); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/DiagnosticDescriptors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace ConsoleAppFramework; 4 | 5 | internal sealed class DiagnosticReporter 6 | { 7 | List? diagnostics; 8 | 9 | public bool HasDiagnostics => diagnostics != null && diagnostics.Count != 0; 10 | 11 | public void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) 12 | { 13 | var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); 14 | if (diagnostics == null) 15 | { 16 | diagnostics = new(); 17 | } 18 | diagnostics.Add(diagnostic); 19 | } 20 | 21 | public void ReportToContext(SourceProductionContext context) 22 | { 23 | if (diagnostics != null) 24 | { 25 | foreach (var item in diagnostics) 26 | { 27 | context.ReportDiagnostic(item); 28 | } 29 | } 30 | } 31 | } 32 | 33 | internal static class DiagnosticDescriptors 34 | { 35 | const string Category = "GenerateConsoleAppFramework"; 36 | 37 | public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) 38 | { 39 | var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); 40 | context.ReportDiagnostic(diagnostic); 41 | } 42 | 43 | public static DiagnosticDescriptor Create(int id, string message) 44 | { 45 | return Create(id, message, message); 46 | } 47 | 48 | public static DiagnosticDescriptor Create(int id, string title, string messageFormat) 49 | { 50 | return new DiagnosticDescriptor( 51 | id: "CAF" + id.ToString("000"), 52 | title: title, 53 | messageFormat: messageFormat, 54 | category: Category, 55 | defaultSeverity: DiagnosticSeverity.Error, 56 | isEnabledByDefault: true); 57 | } 58 | 59 | public static DiagnosticDescriptor RequireArgsAndMethod { get; } = Create( 60 | 1, 61 | "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments."); 62 | 63 | public static DiagnosticDescriptor ReturnTypeLambda { get; } = Create( 64 | 2, 65 | "Command lambda expressions return type must be void or int or async Task or async Task.", 66 | "Command lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); 67 | 68 | public static DiagnosticDescriptor ReturnTypeMethod { get; } = Create( 69 | 3, 70 | "Command method return type must be void or int or async Task or async Task.", 71 | "Command method return type must be void or int or async Task or async Task but returned '{0}'."); 72 | 73 | // v5.7.7 supports non-first argument parameters 74 | //public static DiagnosticDescriptor SequentialArgument { get; } = Create( 75 | // 4, 76 | // "All Argument parameters must be sequential from first."); 77 | 78 | public static DiagnosticDescriptor FunctionPointerCanNotHaveValidation { get; } = Create( 79 | 5, 80 | "Function pointer can not have validation."); 81 | 82 | public static DiagnosticDescriptor AddCommandMustBeStringLiteral { get; } = Create( 83 | 6, 84 | "ConsoleAppBuilder.Add string command must be string literal."); 85 | 86 | public static DiagnosticDescriptor DuplicateCommandName { get; } = Create( 87 | 7, 88 | "Command name is duplicated.", 89 | "Command name '{0}' is duplicated."); 90 | 91 | public static DiagnosticDescriptor AddInLoopIsNotAllowed { get; } = Create( 92 | 8, 93 | "ConsoleAppBuilder.Add/UseFilter is not allowed in loop statements(while, do, for, foreach)."); 94 | 95 | public static DiagnosticDescriptor CommandHasFilter { get; } = Create( 96 | 9, 97 | "ConsoleApp.Run does not allow the use of filters, but the function has a filter attribute."); 98 | 99 | public static DiagnosticDescriptor FilterMultipleConstructor { get; } = Create( 100 | 10, 101 | "ConsoleAppFilter class does not allow multiple constructors."); 102 | 103 | public static DiagnosticDescriptor ClassMultipleConstructor { get; } = Create( 104 | 11, 105 | "ConsoleAppBuilder.Add class does not allow multiple constructors."); 106 | 107 | public static DiagnosticDescriptor ClassHasNoPublicMethods { get; } = Create( 108 | 12, 109 | "ConsoleAppBuilder.Add class must have at least one public method."); 110 | 111 | public static DiagnosticDescriptor ClassIsStaticOrAbstract { get; } = Create( 112 | 13, 113 | "ConsoleAppBuilder.Add class does not allow static or abstract classes."); 114 | 115 | public static DiagnosticDescriptor DefinedInOtherProject { get; } = Create( 116 | 14, 117 | "ConsoleAppFramework cannot register type/method in another project outside the SourceGenerator referenced project."); 118 | 119 | public static DiagnosticDescriptor DocCommentParameterNameNotMatched { get; } = Create( 120 | 15, 121 | "Document Comment parameter name '{0}' does not match method parameter name."); 122 | 123 | public static DiagnosticDescriptor ReturnTypeMethodAsyncVoid { get; } = Create( 124 | 16, 125 | "Command method return type does not allow async void."); 126 | 127 | public static DiagnosticDescriptor DuplicateConfigureGlobalOptions { get; } = Create( 128 | 17, 129 | "ConfigureGlobalOptions does not allow to invoke twice."); 130 | 131 | public static DiagnosticDescriptor InvalidGlobalOptionsType { get; } = Create( 132 | 18, 133 | "GlobalOption parameter type only allows compile-time constant(primitives, string, enum) and there nullable."); 134 | } 135 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | *.publishproj 144 | 145 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 146 | # checkin your Azure Web App publish settings, but sensitive information contained 147 | # in these scripts will be unencrypted 148 | PublishScripts/ 149 | 150 | # NuGet Packages 151 | *.nupkg 152 | # The packages folder can be ignored because of Package Restore 153 | **/packages/* 154 | # except build/, which is used as an MSBuild target. 155 | !**/packages/build/ 156 | # Uncomment if necessary however generally it will be regenerated when needed 157 | #!**/packages/repositories.config 158 | # NuGet v3's project.json files produces more ignoreable files 159 | *.nuget.props 160 | *.nuget.targets 161 | 162 | # Microsoft Azure Build Output 163 | csx/ 164 | *.build.csdef 165 | 166 | # Microsoft Azure Emulator 167 | ecf/ 168 | rcf/ 169 | 170 | # Windows Store app package directories and files 171 | AppPackages/ 172 | BundleArtifacts/ 173 | Package.StoreAssociation.xml 174 | _pkginfo.txt 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | ~$* 185 | *~ 186 | *.dbmdl 187 | *.dbproj.schemaview 188 | *.jfm 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # Since there are multiple workflows, uncomment next line to ignore bower_components 195 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 196 | #bower_components/ 197 | 198 | # RIA/Silverlight projects 199 | Generated_Code/ 200 | 201 | # Backup & report files from converting an old project file 202 | # to a newer Visual Studio version. Backup files are not needed, 203 | # because we have git ;-) 204 | _UpgradeReport_Files/ 205 | Backup*/ 206 | UpgradeLog*.XML 207 | UpgradeLog*.htm 208 | 209 | # SQL Server files 210 | *.mdf 211 | *.ldf 212 | 213 | # Business Intelligence projects 214 | *.rdl.data 215 | *.bim.layout 216 | *.bim_*.settings 217 | 218 | # Microsoft Fakes 219 | FakesAssemblies/ 220 | 221 | # GhostDoc plugin setting file 222 | *.GhostDoc.xml 223 | 224 | # Node.js Tools for Visual Studio 225 | .ntvs_analysis.dat 226 | 227 | # Visual Studio 6 build log 228 | *.plg 229 | 230 | # Visual Studio 6 workspace options file 231 | *.opt 232 | 233 | # Visual Studio LightSwitch build output 234 | **/*.HTMLClient/GeneratedArtifacts 235 | **/*.DesktopClient/GeneratedArtifacts 236 | **/*.DesktopClient/ModelManifest.xml 237 | **/*.Server/GeneratedArtifacts 238 | **/*.Server/ModelManifest.xml 239 | _Pvt_Extensions 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | paket-files/ 244 | 245 | # FAKE - F# Make 246 | .fake/ 247 | 248 | # JetBrains Rider 249 | .idea/ 250 | *.sln.iml 251 | 252 | # CodeRush 253 | .cr/ 254 | 255 | # Python Tools for Visual Studio (PTVS) 256 | __pycache__/ 257 | *.pyc 258 | 259 | # Unity 260 | 261 | src/MessagePack.UnityClient/bin/* 262 | src/MessagePack.UnityClient/Library/* 263 | src/MessagePack.UnityClient/obj/* 264 | src/MessagePack.UnityClient/Temp/* 265 | 266 | # Project Specified 267 | 268 | nuget/mpc.exe 269 | nuget/mpc.exe.config 270 | 271 | nuget/tools/* 272 | nuget/unity/tools/* 273 | nuget/unity* 274 | /nuget/*.unitypackage 275 | 276 | # VSCode 277 | .vscode/* 278 | !.vscode/settings.json 279 | !.vscode/tasks.json 280 | !.vscode/launch.json 281 | !.vscode/extensions.json 282 | sandbox/**/Properties/launchSettings.json 283 | circleci.exe 284 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class FilterTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task ForLambda() 9 | { 10 | await verifier.Execute(""" 11 | var builder = ConsoleApp.Create(); 12 | 13 | builder.UseFilter(); 14 | builder.UseFilter(); 15 | 16 | builder.Add("", Hello); 17 | 18 | builder.Run(args); 19 | 20 | [ConsoleAppFilter] 21 | [ConsoleAppFilter] 22 | void Hello() 23 | { 24 | Console.Write("abcde"); 25 | } 26 | 27 | internal class NopFilter1(ConsoleAppFilter next) 28 | : ConsoleAppFilter(next) 29 | { 30 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 31 | { 32 | Console.Write(1); 33 | return Next.InvokeAsync(context, cancellationToken); 34 | } 35 | } 36 | 37 | internal class NopFilter2(ConsoleAppFilter next) 38 | : ConsoleAppFilter(next) 39 | { 40 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 41 | { 42 | Console.Write(2); 43 | return Next.InvokeAsync(context, cancellationToken); 44 | } 45 | } 46 | 47 | internal class NopFilter3(ConsoleAppFilter next) 48 | : ConsoleAppFilter(next) 49 | { 50 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 51 | { 52 | Console.Write(3); 53 | return Next.InvokeAsync(context, cancellationToken); 54 | } 55 | } 56 | 57 | internal class NopFilter4(ConsoleAppFilter next) 58 | : ConsoleAppFilter(next) 59 | { 60 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 61 | { 62 | Console.Write(4); 63 | return Next.InvokeAsync(context, cancellationToken); 64 | } 65 | } 66 | """, args: "", expected: "1234abcde"); 67 | } 68 | 69 | [Test] 70 | public async Task ForClass() 71 | { 72 | await verifier.Execute(""" 73 | var builder = ConsoleApp.Create(); 74 | 75 | builder.UseFilter(); 76 | builder.UseFilter(); 77 | 78 | builder.Add(); 79 | 80 | await builder.RunAsync(args); 81 | 82 | [ConsoleAppFilter] 83 | [ConsoleAppFilter] 84 | public class MyClass 85 | { 86 | [ConsoleAppFilter] 87 | [ConsoleAppFilter] 88 | public void Hello() 89 | { 90 | Console.Write("abcde"); 91 | } 92 | } 93 | 94 | internal class NopFilter1(ConsoleAppFilter next) 95 | : ConsoleAppFilter(next) 96 | { 97 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 98 | { 99 | Console.Write(1); 100 | return Next.InvokeAsync(context, cancellationToken); 101 | } 102 | } 103 | 104 | internal class NopFilter2(ConsoleAppFilter next) 105 | : ConsoleAppFilter(next) 106 | { 107 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 108 | { 109 | Console.Write(2); 110 | return Next.InvokeAsync(context, cancellationToken); 111 | } 112 | } 113 | 114 | internal class NopFilter3(ConsoleAppFilter next) 115 | : ConsoleAppFilter(next) 116 | { 117 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 118 | { 119 | Console.Write(3); 120 | return Next.InvokeAsync(context, cancellationToken); 121 | } 122 | } 123 | 124 | internal class NopFilter4(ConsoleAppFilter next) 125 | : ConsoleAppFilter(next) 126 | { 127 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 128 | { 129 | Console.Write(4); 130 | return Next.InvokeAsync(context, cancellationToken); 131 | } 132 | } 133 | 134 | internal class NopFilter5(ConsoleAppFilter next) 135 | : ConsoleAppFilter(next) 136 | { 137 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 138 | { 139 | Console.Write(5); 140 | return Next.InvokeAsync(context, cancellationToken); 141 | } 142 | } 143 | 144 | internal class NopFilter6(ConsoleAppFilter next) 145 | : ConsoleAppFilter(next) 146 | { 147 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 148 | { 149 | Console.Write(6); 150 | return Next.InvokeAsync(context, cancellationToken); 151 | } 152 | } 153 | """, args: "hello", expected: "123456abcde"); 154 | } 155 | 156 | 157 | [Test] 158 | public async Task DI() 159 | { 160 | await verifier.Execute(""" 161 | var serviceCollection = new MiniDI(); 162 | serviceCollection.Register(typeof(string), "hoge!"); 163 | serviceCollection.Register(typeof(int), 9999); 164 | ConsoleApp.ServiceProvider = serviceCollection; 165 | 166 | var builder = ConsoleApp.Create(); 167 | 168 | builder.UseFilter(); 169 | 170 | builder.Add("", () => Console.Write("do")); 171 | 172 | builder.Run(args); 173 | 174 | internal class DIFilter(string foo, int bar, ConsoleAppFilter next) 175 | : ConsoleAppFilter(next) 176 | { 177 | public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 178 | { 179 | Console.Write("invoke:"); 180 | Console.Write(foo); 181 | Console.Write(bar); 182 | return Next.InvokeAsync(context, cancellationToken); 183 | } 184 | } 185 | 186 | public class MiniDI : IServiceProvider 187 | { 188 | System.Collections.Generic.Dictionary dict = new(); 189 | 190 | public void Register(Type type, object instance) 191 | { 192 | dict[type] = instance; 193 | } 194 | 195 | public object? GetService(Type serviceType) 196 | { 197 | return dict.TryGetValue(serviceType, out var instance) ? instance : null; 198 | } 199 | } 200 | """, args: "", expected: "invoke:hoge!9999do"); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/GlobalOptionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleAppFramework.GeneratorTests; 8 | 9 | public class GlobalOptionTest 10 | { 11 | VerifyHelper verifier = new VerifyHelper("CAF"); 12 | 13 | [Test] 14 | public async Task BooleanParseCheck() 15 | { 16 | string BuildCode(string parameter) 17 | { 18 | return $$""" 19 | var app = ConsoleApp.Create(); 20 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => builder.AddGlobalOption("{{parameter}}", "")); 21 | app.Add("", (ConsoleAppContext context) => Console.Write(context.GlobalOptions)); 22 | app.Run(args); 23 | """; 24 | } 25 | 26 | await verifier.Execute(BuildCode("-v"), "-v", "True"); 27 | await verifier.Execute(BuildCode("-no"), "-v", "False"); 28 | await verifier.Execute(BuildCode("-v|--verbose"), "-v", "True"); 29 | await verifier.Execute(BuildCode("-v|--verbose"), "--verbose", "True"); 30 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "-v", "True"); 31 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--verbose", "True"); 32 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--vo-v", "True"); 33 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--no", "False"); 34 | } 35 | 36 | [Test] 37 | public async Task ArgumentRemove() 38 | { 39 | var code = $$""" 40 | var app = ConsoleApp.Create(); 41 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 42 | { 43 | var p = builder.AddGlobalOption("--parameter", "", 0); 44 | return p; 45 | }); 46 | app.Add("", (int x, int y, ConsoleAppContext context) => 47 | { 48 | Console.Write($"{context.GlobalOptions} -> ({x}, {y})"); 49 | }); 50 | app.Run(args); 51 | """; 52 | 53 | // first 54 | await verifier.Execute(code, "--parameter 100 --x 10 --y 20", "100 -> (10, 20)"); 55 | 56 | // middle 57 | await verifier.Execute(code, "--x 10 --parameter 100 --y 20", "100 -> (10, 20)"); 58 | 59 | // last 60 | await verifier.Execute(code, "--x 10 --y 20 --parameter 100", "100 -> (10, 20)"); 61 | } 62 | 63 | [Test] 64 | public async Task EnumParse() 65 | { 66 | await verifier.Execute(""" 67 | var app = ConsoleApp.Create(); 68 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 69 | { 70 | var p = builder.AddGlobalOption("--parameter", "", 0); 71 | var d = builder.AddGlobalOption("--dry-run", ""); 72 | var f = builder.AddGlobalOption("--fruit", "", Fruit.Orange); 73 | return (p, d, f); 74 | }); 75 | app.Add("", (int x, int y, ConsoleAppContext context) => 76 | { 77 | Console.Write($"{context.GlobalOptions} -> {(x, y)}"); 78 | }); 79 | app.Run(args); 80 | 81 | enum Fruit 82 | { 83 | Orange, Apple, Grape 84 | } 85 | """, "--parameter 100 --x 10 --dry-run --y 20 --fruit grape", "(100, True, Grape) -> (10, 20)"); 86 | } 87 | 88 | [Test] 89 | public async Task EnumErrorShowsValidValues() 90 | { 91 | var result = verifier.Error(""" 92 | ConsoleApp.Log = x => Console.Write(x); 93 | 94 | var app = ConsoleApp.Create(); 95 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 96 | { 97 | var fruit = builder.AddGlobalOption("--fruit", "", Fruit.Apple); 98 | return fruit; 99 | }); 100 | 101 | app.Add("", (ConsoleAppContext context) => { }); 102 | app.Run(args); 103 | 104 | enum Fruit 105 | { 106 | Orange, Apple, Grape 107 | } 108 | """, "--fruit Potato"); 109 | 110 | await Assert.That(result.Stdout).Contains("Argument '--fruit' is invalid. Provided value: Potato. Valid values: Orange, Apple, Grape"); 111 | await Assert.That(result.ExitCode).IsEqualTo(1); 112 | } 113 | 114 | [Test] 115 | public async Task DefaultValueForOption() 116 | { 117 | await verifier.Execute(""" 118 | var app = ConsoleApp.Create(); 119 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 120 | { 121 | var p = builder.AddGlobalOption("--parameter", "", -10); 122 | var d = builder.AddGlobalOption("--dry-run", ""); 123 | var f = builder.AddGlobalOption("--fruit", "", Fruit.Apple); 124 | return (p, d, f); 125 | }); 126 | app.Add("", (int x, int y, ConsoleAppContext context) => 127 | { 128 | Console.Write($"{context.GlobalOptions} -> {(x, y)}"); 129 | }); 130 | app.Run(args); 131 | 132 | enum Fruit 133 | { 134 | Orange, Apple, Grape 135 | } 136 | 137 | """, "--x 10 --y 20", "(-10, False, Apple) -> (10, 20)"); 138 | } 139 | 140 | [Test] 141 | public async Task RequiredParse() 142 | { 143 | var error = verifier.Error(""" 144 | var app = ConsoleApp.Create(); 145 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 146 | { 147 | var p = builder.AddRequiredGlobalOption("--parameter", ""); 148 | var d = builder.AddGlobalOption("--dry-run", ""); 149 | return (p, d); 150 | }); 151 | app.Add("", (int x, int y, ConsoleAppContext context) => 152 | { 153 | Console.Write($"{context.GlobalOptions} -> {(x, y)}"); 154 | }); 155 | app.Run(args); 156 | """, "--x 10 --dry-run --y 20"); 157 | 158 | error.Stdout.Contains("Required argument '--parameter' was not specified."); 159 | } 160 | 161 | [Test] 162 | public async Task NamedParameter() 163 | { 164 | await verifier.Execute(""" 165 | var app = ConsoleApp.Create(); 166 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 167 | { 168 | var p = builder.AddGlobalOption("--parameter", defaultValue: 1000); 169 | var d = builder.AddGlobalOption(description: "foo", name: "--dry-run"); 170 | return (p, d); 171 | }); 172 | app.Add("", (int x, int y, ConsoleAppContext context) => 173 | { 174 | Console.Write($"{context.GlobalOptions} -> {(x, y)}"); 175 | }); 176 | app.Run(args); 177 | """, "--x 10 --dry-run --y 20", "(1000, True) -> (10, 20)"); 178 | } 179 | 180 | [Test] 181 | public async Task DoubleDashEscape() 182 | { 183 | await verifier.Execute(""" 184 | var app = ConsoleApp.Create(); 185 | 186 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 187 | { 188 | var flag = builder.AddGlobalOption("--global-flag"); 189 | return new GlobalOptions(flag); 190 | }); 191 | 192 | app.UseFilter(); 193 | app.Add(); 194 | app.Run(args); 195 | 196 | internal record GlobalOptions(string Flag); 197 | 198 | internal class Commands 199 | { 200 | [Command("some-command")] 201 | public async Task SomeCommand([Argument] string commandArg, ConsoleAppContext context) 202 | { 203 | Console.WriteLine($"ARG: {commandArg}"); 204 | Console.WriteLine($"ESCAPED: {string.Join(", ", context.EscapedArguments.ToArray()!)}"); 205 | } 206 | } 207 | 208 | internal class SomeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) 209 | { 210 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) 211 | { 212 | Console.WriteLine($"FLAG: {((GlobalOptions)context.GlobalOptions!).Flag}"); 213 | await Next.InvokeAsync(context, cancellationToken); 214 | } 215 | } 216 | """, "some-command hello --global-flag flag-value -- more args here", """ 217 | FLAG: flag-value 218 | ARG: hello 219 | ESCAPED: more, args, here 220 | 221 | """); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class ConsoleAppBuilderTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task BuilderRun() 9 | { 10 | var code = """ 11 | var builder = ConsoleApp.Create(); 12 | builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); 13 | builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); 14 | builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); 15 | builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); }); 16 | builder.Run(args); 17 | """; 18 | await verifier.Execute(code, "foo --x 10 --y 20", "30"); 19 | await verifier.Execute(code, "bar --x 20 --y 30", "50"); 20 | var exitCode = await verifier.Execute(code, "bar --x 20", "30"); 21 | await Assert.That(exitCode).IsZero(); 22 | exitCode = await verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki"); 23 | await Assert.That(exitCode).IsEqualTo(10); 24 | 25 | await verifier.Execute(code, "boz --x 40", "80"); 26 | } 27 | 28 | [Test] 29 | public async Task BuilderRunAsync() 30 | { 31 | var code = """ 32 | var builder = ConsoleApp.Create(); 33 | builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); 34 | builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); 35 | builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); 36 | builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); }); 37 | await builder.RunAsync(args); 38 | """; 39 | 40 | await verifier.Execute(code, "foo --x 10 --y 20", "30"); 41 | await verifier.Execute(code, "bar --x 20 --y 30", "50"); 42 | var exitCode = await verifier.Execute(code, "bar --x 20", "30"); 43 | await Assert.That(exitCode).IsZero(); 44 | exitCode = await verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki"); 45 | await Assert.That(exitCode).IsEqualTo(10); 46 | 47 | await verifier.Execute(code, "boz --x 40", "80"); 48 | } 49 | 50 | [Test] 51 | public async Task AddClass() 52 | { 53 | var code = """ 54 | var builder = ConsoleApp.Create(); 55 | builder.Add(); 56 | await builder.RunAsync(args); 57 | 58 | public class MyClass 59 | { 60 | public void Do() 61 | { 62 | Console.Write("yeah"); 63 | } 64 | 65 | public void Sum(int x, int y) 66 | { 67 | Console.Write(x + y); 68 | } 69 | 70 | public void Echo(string msg) 71 | { 72 | Console.Write(msg); 73 | } 74 | 75 | void Echo() 76 | { 77 | } 78 | 79 | public static void Sum() 80 | { 81 | } 82 | } 83 | """; 84 | 85 | await verifier.Execute(code, "do", "yeah"); 86 | await verifier.Execute(code, "sum --x 1 --y 2", "3"); 87 | await verifier.Execute(code, "echo --msg takoyaki", "takoyaki"); 88 | } 89 | 90 | [Test] 91 | public async Task ClassDispose() 92 | { 93 | await verifier.Execute(""" 94 | var builder = ConsoleApp.Create(); 95 | builder.Add(); 96 | builder.Run(args); 97 | 98 | public class MyClass : IDisposable 99 | { 100 | public void Do() 101 | { 102 | Console.Write("yeah:"); 103 | } 104 | 105 | public void Dispose() 106 | { 107 | Console.Write("disposed!"); 108 | } 109 | } 110 | """, "do", "yeah:disposed!"); 111 | 112 | await verifier.Execute(""" 113 | var builder = ConsoleApp.Create(); 114 | builder.Add(); 115 | await builder.RunAsync(args); 116 | 117 | public class MyClass : IDisposable 118 | { 119 | public void Do() 120 | { 121 | Console.Write("yeah:"); 122 | } 123 | 124 | public void Dispose() 125 | { 126 | Console.Write("disposed!"); 127 | } 128 | } 129 | """, "do", "yeah:disposed!"); 130 | 131 | await verifier.Execute(""" 132 | var builder = ConsoleApp.Create(); 133 | builder.Add(); 134 | await builder.RunAsync(args); 135 | 136 | public class MyClass : IAsyncDisposable 137 | { 138 | public void Do() 139 | { 140 | Console.Write("yeah:"); 141 | } 142 | 143 | public ValueTask DisposeAsync() 144 | { 145 | Console.Write("disposed!"); 146 | return default; 147 | } 148 | } 149 | """, "do", "yeah:disposed!"); 150 | 151 | // DisposeAsync: sync pattern 152 | await verifier.Execute(""" 153 | var builder = ConsoleApp.Create(); 154 | builder.Add(); 155 | builder.Run(args); 156 | 157 | public class MyClass : IAsyncDisposable 158 | { 159 | public void Do() 160 | { 161 | Console.Write("yeah:"); 162 | } 163 | 164 | public ValueTask DisposeAsync() 165 | { 166 | Console.Write("disposed!"); 167 | return default; 168 | } 169 | } 170 | """, "do", "yeah:disposed!"); 171 | } 172 | 173 | [Test] 174 | public async Task ClassWithDI() 175 | { 176 | await verifier.Execute(""" 177 | var serviceCollection = new MiniDI(); 178 | serviceCollection.Register(typeof(string), "hoge!"); 179 | serviceCollection.Register(typeof(int), 9999); 180 | ConsoleApp.ServiceProvider = serviceCollection; 181 | 182 | var builder = ConsoleApp.Create(); 183 | builder.Add(); 184 | builder.Run(args); 185 | 186 | public class MyClass(string foo, int bar) 187 | { 188 | public void Do() 189 | { 190 | Console.Write("yeah:"); 191 | Console.Write(foo); 192 | Console.Write(bar); 193 | } 194 | } 195 | 196 | public class MiniDI : IServiceProvider 197 | { 198 | System.Collections.Generic.Dictionary dict = new(); 199 | 200 | public void Register(Type type, object instance) 201 | { 202 | dict[type] = instance; 203 | } 204 | 205 | public object GetService(Type serviceType) 206 | { 207 | return dict.TryGetValue(serviceType, out var instance) ? instance : null; 208 | } 209 | } 210 | """, "do", "yeah:hoge!9999"); 211 | } 212 | 213 | [Test] 214 | public async Task CommandAttr() 215 | { 216 | var code = """ 217 | var builder = ConsoleApp.Create(); 218 | builder.Add(); 219 | builder.Run(args); 220 | 221 | public class MyClass() 222 | { 223 | [Command("nomunomu")] 224 | public void Do() 225 | { 226 | Console.Write("yeah"); 227 | } 228 | } 229 | """; 230 | 231 | await verifier.Execute(code, "nomunomu", "yeah"); 232 | } 233 | 234 | [Test] 235 | public async Task CommandAttrWithFilter() 236 | { 237 | var code = """ 238 | var builder = ConsoleApp.Create(); 239 | builder.Add(); 240 | builder.Run(args); 241 | 242 | public class MyClass() 243 | { 244 | [ConsoleAppFilter] 245 | [Command("nomunomu")] 246 | [ConsoleAppFilter] 247 | public void Do() 248 | { 249 | Console.Write("command"); 250 | } 251 | } 252 | 253 | internal class NopFilter1(ConsoleAppFilter next) 254 | : ConsoleAppFilter(next) 255 | { 256 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 257 | { 258 | Console.Write("filter1-"); 259 | return Next.InvokeAsync(context, cancellationToken); 260 | } 261 | } 262 | internal class NopFilter2(ConsoleAppFilter next) 263 | : ConsoleAppFilter(next) 264 | { 265 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) 266 | { 267 | Console.Write("filter2-"); 268 | return Next.InvokeAsync(context, cancellationToken); 269 | } 270 | } 271 | """; 272 | 273 | await verifier.Execute(code, "nomunomu", "filter1-filter2-command"); 274 | } 275 | 276 | [Test] 277 | public async Task CommandAlias() 278 | { 279 | var code = """ 280 | var app = ConsoleApp.Create(); 281 | 282 | app.Add("build|b", () => { Console.Write("build ok"); }); 283 | app.Add("test|t", () => { Console.Write("test ok"); }); 284 | app.Add(); 285 | 286 | app.Run(args); 287 | 288 | public class Commands 289 | { 290 | /// Analyze the current package and report errors, but don't build object files. 291 | [Command("check|c")] 292 | public void Check() { Console.Write("check ok"); } 293 | 294 | /// Build this packages's and its dependencies' documenation. 295 | [Command("doc|d")] 296 | public void Doc() { Console.Write("doc ok"); } 297 | } 298 | """; 299 | 300 | await verifier.Execute(code, "b", "build ok"); 301 | await verifier.Execute(code, "build", "build ok"); 302 | await verifier.Execute(code, "t", "test ok"); 303 | await verifier.Execute(code, "test", "test ok"); 304 | await verifier.Execute(code, "c", "check ok"); 305 | await verifier.Execute(code, "check", "check ok"); 306 | await verifier.Execute(code, "d", "doc ok"); 307 | await verifier.Execute(code, "doc", "doc ok"); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/RoslynExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | 5 | namespace ConsoleAppFramework; 6 | 7 | internal static class RoslynExtensions 8 | { 9 | internal static string ToFullyQualifiedFormatDisplayString(this ITypeSymbol typeSymbol) 10 | { 11 | return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 12 | } 13 | 14 | public static bool EqualsUnconstructedGenericType(this INamedTypeSymbol left, INamedTypeSymbol right) 15 | { 16 | var l = left.IsGenericType ? left.ConstructUnboundGenericType() : left; 17 | var r = right.IsGenericType ? right.ConstructUnboundGenericType() : right; 18 | return SymbolEqualityComparer.Default.Equals(l, r); 19 | } 20 | 21 | public static IEnumerable GetBaseTypes(this INamedTypeSymbol type, bool includeSelf = false) 22 | { 23 | if (includeSelf) yield return type; 24 | var baseType = type.BaseType; 25 | while (baseType != null) 26 | { 27 | yield return baseType; 28 | baseType = baseType.BaseType; 29 | } 30 | } 31 | 32 | public static bool EqualsNamespaceAndName(this ITypeSymbol? left, ITypeSymbol? right) 33 | { 34 | if (left == null && right == null) return true; 35 | if (left == null || right == null) return false; 36 | 37 | var l = left.ContainingNamespace; 38 | var r = right.ContainingNamespace; 39 | while (l != null && r != null) 40 | { 41 | if (l.Name != r.Name) return false; 42 | 43 | l = l.ContainingNamespace; 44 | r = r.ContainingNamespace; 45 | } 46 | 47 | return (left.Name == right.Name); 48 | } 49 | 50 | public static bool ZipEquals(this IEnumerable left, IEnumerable right, Func predicate) 51 | where T : IMethodSymbol 52 | { 53 | using var e1 = left.GetEnumerator(); 54 | using var e2 = right.GetEnumerator(); 55 | while (true) 56 | { 57 | var b1 = e1.MoveNext(); 58 | var b2 = e2.MoveNext(); 59 | 60 | if (b1 != b2) return false; // different sequence length, ng 61 | if (b1 == false) return true; // both false, ok 62 | 63 | if (!predicate(e1.Current, e2.Current)) 64 | { 65 | return false; 66 | } 67 | } 68 | } 69 | 70 | public static ParameterListSyntax? GetParameterListOfConstructor(this SyntaxNode node) 71 | { 72 | if (node is ConstructorDeclarationSyntax ctor) 73 | { 74 | return ctor.ParameterList; 75 | } 76 | else if (node is ClassDeclarationSyntax primaryCtor) 77 | { 78 | return primaryCtor.ParameterList; 79 | } 80 | else 81 | { 82 | return null; 83 | } 84 | } 85 | 86 | public static Location Clone(this Location location) 87 | { 88 | // without inner SyntaxTree 89 | return Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span); 90 | } 91 | 92 | public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) 93 | { 94 | // Hack note: 95 | // ISymbol.GetDocumentationCommentXml requirestrue. 96 | // However, getting the DocumentationCommentTrivia of a SyntaxNode also requires the same condition. 97 | // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse, 98 | // it becomes None, and the necessary Trivia cannot be obtained. 99 | // Therefore, we will attempt to reparse and retrieve it. 100 | 101 | // About DocumentationMode and Trivia: https://github.com/dotnet/roslyn/issues/58210 102 | if (node.SyntaxTree.Options.DocumentationMode == DocumentationMode.None) 103 | { 104 | var withDocumentationComment = node.SyntaxTree.Options.WithDocumentationMode(DocumentationMode.Parse); 105 | var code = node.ToFullString(); 106 | var newTree = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)withDocumentationComment); 107 | node = newTree.GetRoot(); 108 | } 109 | 110 | foreach (var leadingTrivia in node.GetLeadingTrivia()) 111 | { 112 | if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure) 113 | { 114 | return structure; 115 | } 116 | } 117 | 118 | return null; 119 | } 120 | 121 | static IEnumerable GetXmlElements(this SyntaxList content, string elementName) 122 | { 123 | foreach (XmlNodeSyntax syntax in content) 124 | { 125 | if (syntax is XmlEmptyElementSyntax emptyElement) 126 | { 127 | if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal)) 128 | { 129 | yield return emptyElement; 130 | } 131 | 132 | continue; 133 | } 134 | 135 | if (syntax is XmlElementSyntax elementSyntax) 136 | { 137 | if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal)) 138 | { 139 | yield return elementSyntax; 140 | } 141 | 142 | continue; 143 | } 144 | } 145 | } 146 | 147 | public static string GetSummary(this DocumentationCommentTriviaSyntax docComment) 148 | { 149 | var summary = docComment.Content.GetXmlElements("summary").FirstOrDefault() as XmlElementSyntax; 150 | if (summary == null) return ""; 151 | 152 | return summary.Content.ToString().Replace("///", "").Trim(); 153 | } 154 | 155 | public static IEnumerable<(string Name, string Description)> GetParams(this DocumentationCommentTriviaSyntax docComment) 156 | { 157 | foreach (var item in docComment.Content.GetXmlElements("param").OfType()) 158 | { 159 | var name = item.StartTag.Attributes.OfType().FirstOrDefault()?.Identifier.Identifier.ValueText.Replace("///", "").Trim() ?? ""; 160 | var desc = item.Content.ToString().Replace("///", "").Trim() ?? ""; 161 | yield return (name, desc); 162 | } 163 | 164 | yield break; 165 | } 166 | 167 | public static void GetConstantValues(this ArgumentListSyntax argumentListSyntax, SemanticModel model, 168 | string name1, string name2, 169 | ref object? value1, ref object? value2) 170 | { 171 | var arguments = argumentListSyntax.Arguments; 172 | for (int i = 0; i < arguments.Count; i++) 173 | { 174 | var arg = arguments[i]; 175 | var constant = model.GetConstantValue(arg.Expression); 176 | if (constant.HasValue) 177 | { 178 | var constantValue = constant.Value; 179 | if (arg.NameColon != null) 180 | { 181 | var name = arg.NameColon.Name.Identifier.Text; 182 | if (name == name1) 183 | { 184 | value1 = constantValue; 185 | } 186 | else if (name == name2) 187 | { 188 | value2 = constantValue; 189 | } 190 | } 191 | else 192 | { 193 | if (i == 0) value1 = constantValue; 194 | else if (i == 1) value2 = constantValue; 195 | } 196 | } 197 | } 198 | } 199 | 200 | public static void GetConstantValues(this ArgumentListSyntax argumentListSyntax, SemanticModel model, 201 | string name1, string name2, string name3, 202 | ref object? value1, ref object? value2, ref object? value3) 203 | { 204 | var arguments = argumentListSyntax.Arguments; 205 | for (int i = 0; i < arguments.Count; i++) 206 | { 207 | var arg = arguments[i]; 208 | var constant = model.GetConstantValue(arg.Expression); 209 | if (constant.HasValue) 210 | { 211 | var constantValue = constant.Value; 212 | if (arg.NameColon != null) 213 | { 214 | var name = arg.NameColon.Name.Identifier.Text; 215 | if (name == name1) 216 | { 217 | value1 = constantValue; 218 | } 219 | else if (name == name2) 220 | { 221 | value2 = constantValue; 222 | } 223 | else if (name == name3) 224 | { 225 | value3 = constantValue; 226 | } 227 | } 228 | else 229 | { 230 | if (i == 0) value1 = constantValue; 231 | else if (i == 1) value2 = constantValue; 232 | else if (i == 2) value3 = constantValue; 233 | } 234 | } 235 | } 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/RunTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class Test 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task SyncRun() 9 | { 10 | await verifier.Execute("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 --y 20", "30"); 11 | } 12 | 13 | [Test] 14 | public async Task OptionTokenShouldNotFillArgumentSlot() 15 | { 16 | var code = """ 17 | ConsoleApp.Log = x => Console.Write(x); 18 | ConsoleApp.Run(args, ([Argument] string path, bool dryRun) => 19 | { 20 | Console.Write((dryRun, path).ToString()); 21 | }); 22 | """; 23 | 24 | await Assert.That(verifier.Error(code, "--dry-run").Stdout).Contains("Required argument 'path' was not specified."); 25 | await verifier.Execute(code, "--dry-run sample.txt", "(True, sample.txt)"); 26 | } 27 | 28 | [Test] 29 | public async Task OptionTokenAllowsMultipleArguments() 30 | { 31 | var code = """ 32 | ConsoleApp.Log = x => Console.Write(x); 33 | ConsoleApp.Run(args, ([Argument] string source, [Argument] string destination, bool dryRun) => 34 | { 35 | Console.Write((dryRun, source, destination).ToString()); 36 | }); 37 | """; 38 | 39 | await verifier.Execute(code, "--dry-run input.json output.json", "(True, input.json, output.json)"); 40 | } 41 | 42 | [Test] 43 | public async Task OptionTokenRespectsArgumentDefaultValue() 44 | { 45 | var code = """ 46 | ConsoleApp.Run(args, ([Argument] string path = "default-path", bool dryRun = false) => 47 | { 48 | Console.Write((dryRun, path).ToString()); 49 | }); 50 | """; 51 | 52 | await verifier.Execute(code, "--dry-run", "(True, default-path)"); 53 | } 54 | 55 | [Test] 56 | public async Task OptionTokenHandlesParamsArguments() 57 | { 58 | var code = """ 59 | ConsoleApp.Run(args, ([Argument] string path, bool dryRun, params string[] extras) => 60 | { 61 | Console.Write($"{dryRun}:{path}:{string.Join("|", extras)}"); 62 | }); 63 | """; 64 | 65 | await verifier.Execute(code, "--dry-run path.txt --extras src.txt dst.txt", "True:path.txt:src.txt|dst.txt"); 66 | await verifier.Execute(code, "--dry-run path.txt", "True:path.txt:"); 67 | } 68 | 69 | [Test] 70 | public async Task ArgumentAllowsLeadingDashValue() 71 | { 72 | var code = """ 73 | ConsoleApp.Run(args, ([Argument] int count, bool dryRun) => 74 | { 75 | Console.Write((count, dryRun).ToString()); 76 | }); 77 | """; 78 | 79 | await verifier.Execute(code, "-5 --dry-run", "(-5, True)"); 80 | await verifier.Execute(code, "-5", "(-5, False)"); 81 | } 82 | 83 | [Test] 84 | public async Task SyncRunShouldFailed() 85 | { 86 | await Assert.That(verifier.Error(""" 87 | ConsoleApp.Log = x => Console.Write(x); 88 | ConsoleApp.Run(args, (int x) => { Console.Write((x)); }); 89 | """, "--x").Stdout).Contains("Argument 'x' failed to parse"); 90 | 91 | } 92 | 93 | [Test] 94 | public async Task EnumErrorShowsValidValues() 95 | { 96 | var result = verifier.Error(""" 97 | ConsoleApp.Log = x => Console.Write(x); 98 | ConsoleApp.Run(args, (Fruit fruit) => { Console.Write(fruit); }); 99 | 100 | enum Fruit 101 | { 102 | Orange, Grape, Apple 103 | } 104 | """, "--fruit Potato"); 105 | 106 | await Assert.That(result.Stdout).Contains("Argument 'fruit' is invalid. Provided value: Potato. Valid values: Orange, Grape, Apple"); 107 | await Assert.That(result.ExitCode).IsEqualTo(1); 108 | } 109 | 110 | [Test] 111 | public async Task MissingArgument() 112 | { 113 | var result = verifier.Error(""" 114 | ConsoleApp.Log = x => Console.Write(x); 115 | ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); }); 116 | """, "--x 10 y 20"); 117 | await Assert.That(result.Stdout).Contains("Argument 'y' is not recognized."); 118 | await Assert.That(result.ExitCode).IsEqualTo(1); 119 | } 120 | 121 | [Test] 122 | public async Task ValidateOne() 123 | { 124 | var expected = """ 125 | The field x must be between 1 and 10. 126 | 127 | """; 128 | 129 | var exitCode = await verifier.Execute(""" 130 | ConsoleApp.Log = x => Console.Write(x); 131 | ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); 132 | """, "--x 100 --y 140", expected); 133 | 134 | await Assert.That(exitCode).IsEqualTo(1); 135 | } 136 | 137 | [Test] 138 | public async Task ValidateTwo() 139 | { 140 | var expected = """ 141 | The field x must be between 1 and 10. 142 | The field y must be between 100 and 200. 143 | 144 | """; 145 | 146 | var exitCode = await verifier.Execute(""" 147 | ConsoleApp.Log = x => Console.Write(x); 148 | ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); 149 | """, "--x 100 --y 240", expected); 150 | 151 | await Assert.That(exitCode).IsEqualTo(1); 152 | } 153 | [Test] 154 | public async Task Parameters() 155 | { 156 | await verifier.Execute(""" 157 | ConsoleApp.Log = x => Console.Write(x); 158 | ConsoleApp.Run(args, (int foo, string bar, Fruit ft, bool flag, Half half, int? itt, Takoyaki.Obj obj) => 159 | { 160 | Console.Write(foo); 161 | Console.Write(bar); 162 | Console.Write(ft); 163 | Console.Write(flag); 164 | Console.Write(half); 165 | Console.Write(itt); 166 | Console.Write(obj.Foo); 167 | }); 168 | 169 | enum Fruit 170 | { 171 | Orange, Grape, Apple 172 | } 173 | 174 | namespace Takoyaki 175 | { 176 | public class Obj 177 | { 178 | public int Foo { get; set; } 179 | } 180 | } 181 | """, "--foo 10 --bar aiueo --ft Grape --flag --half 1.3 --itt 99 --obj {\"Foo\":1999}", "10aiueoGrapeTrue1.3991999"); 182 | } 183 | 184 | [Test] 185 | public async Task ValidateClass() 186 | { 187 | var expected = """ 188 | The field value must be between 0 and 1. 189 | 190 | """; 191 | 192 | await verifier.Execute(""" 193 | ConsoleApp.Log = x => Console.Write(x); 194 | var app = ConsoleApp.Create(); 195 | app.Add(); 196 | app.Run(args); 197 | 198 | public class Test 199 | { 200 | public async Task Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}"); 201 | } 202 | 203 | """, "show --aaa foo --value 100", expected); 204 | 205 | } 206 | 207 | [Test] 208 | public async Task StringEscape() 209 | { 210 | var code = """ 211 | var app = ConsoleApp.Create(); 212 | app.Add(); 213 | app.Run(args); 214 | 215 | public class MyCommands 216 | { 217 | [Command("Error1")] 218 | public async Task Error1(string msg = @"\") 219 | { 220 | Console.Write(msg); 221 | } 222 | [Command("Error2")] 223 | public async Task Error2(string msg = "\\") 224 | { 225 | Console.Write(msg); 226 | } 227 | [Command("Output")] 228 | public async Task Output(string msg = @"\\") 229 | { 230 | Console.Write(msg); 231 | } 232 | } 233 | """; 234 | 235 | await verifier.Execute(code, "Error1", @"\"); 236 | await verifier.Execute(code, "Error2", "\\"); 237 | await verifier.Execute(code, "Output", @"\\"); 238 | 239 | // lambda 240 | 241 | await verifier.Execute(""" 242 | ConsoleApp.Run(args, (string msg = @"\") => Console.Write(msg)); 243 | """, "", @"\"); 244 | 245 | await verifier.Execute(""" 246 | ConsoleApp.Run(args, (string msg = "\\") => Console.Write(msg)); 247 | """, "", "\\"); 248 | 249 | await verifier.Execute(""" 250 | ConsoleApp.Run(args, (string msg = @"\\") => Console.Write(msg)); 251 | """, "", @"\\"); 252 | } 253 | 254 | [Test] 255 | public async Task ShortNameAlias() 256 | { 257 | var code = """ 258 | var app = ConsoleApp.Create(); 259 | app.Add(); 260 | app.Run(args); 261 | 262 | public class FileCommand 263 | { 264 | /// Outputs the provided file name. 265 | /// -i, InputFile 266 | [Command("")] 267 | public async Task Run(string inputFile) => Console.Write(inputFile); 268 | } 269 | """; 270 | 271 | await verifier.Execute(code, "--input-file sample.txt", "sample.txt"); 272 | await verifier.Execute(code, "-i sample.txt", "sample.txt"); 273 | } 274 | 275 | [Test] 276 | public async Task ShortNameAndLongNameAlias() 277 | { 278 | var code = """ 279 | var app = ConsoleApp.Create(); 280 | app.Add(); 281 | app.Run(args); 282 | 283 | public class FileCommand 284 | { 285 | /// Outputs the provided file name. 286 | /// -i|--input, InputFile 287 | [Command("")] 288 | public async Task Run(string inputFile) => Console.Write(inputFile); 289 | } 290 | """; 291 | 292 | await verifier.Execute(code, "--input-file sample.txt", "sample.txt"); 293 | await verifier.Execute(code, "--input sample.txt", "sample.txt"); 294 | await verifier.Execute(code, "-i sample.txt", "sample.txt"); 295 | } 296 | 297 | [Test] 298 | public async Task ArgumentLastParams() 299 | { 300 | var code = """ 301 | ConsoleApp.Run(args, (string opt1, [Argument]params string[] args) => 302 | { 303 | Console.Write($"{opt1}, {string.Join("|", args)}"); 304 | }); 305 | """; 306 | 307 | await verifier.Execute(code, "--opt1 abc a b c d", "abc, a|b|c|d"); 308 | } 309 | 310 | [Test] 311 | public async Task RunAndRunAsyncOverloads() 312 | { 313 | await verifier.Execute(""" 314 | ConsoleApp.Run(args, () => Console.Write("sync")); 315 | """, "", "sync"); 316 | 317 | await verifier.Execute(""" 318 | ConsoleApp.Run(args, async () => Console.Write("async")); 319 | """, "", "async"); 320 | 321 | await verifier.Execute(""" 322 | await ConsoleApp.RunAsync(args, () => Console.Write("sync")); 323 | """, "", "sync"); 324 | 325 | await verifier.Execute(""" 326 | await ConsoleApp.RunAsync(args, async () => Console.Write("async")); 327 | """, "", "async"); 328 | } 329 | 330 | [Test] 331 | public async Task RunAndRunAsyncOverloadsWithCancellationToken() 332 | { 333 | await verifier.Execute(""" 334 | ConsoleApp.Run(args, (CancellationToken cancellationToken) => Console.Write("sync")); 335 | """, "", "sync"); 336 | 337 | await verifier.Execute(""" 338 | ConsoleApp.Run(args, async (CancellationToken cancellationToken) => Console.Write("async")); 339 | """, "", "async"); 340 | 341 | await verifier.Execute(""" 342 | await ConsoleApp.RunAsync(args, (CancellationToken cancellationToken) => Console.Write("sync")); 343 | """, "", "sync"); 344 | 345 | await verifier.Execute(""" 346 | await ConsoleApp.RunAsync(args, async (CancellationToken cancellationToken) => Console.Write("async")); 347 | """, "", "async"); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs: -------------------------------------------------------------------------------- 1 | using ConsoleAppFramework; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Reflection; 8 | using System.Runtime.CompilerServices; 9 | using System.Runtime.Loader; 10 | 11 | public static class CSharpGeneratorRunner 12 | { 13 | static Compilation baseCompilation = default!; 14 | 15 | [ModuleInitializer] 16 | public static void InitializeCompilation() 17 | { 18 | var globalUsings = """ 19 | global using System; 20 | global using System.Threading; 21 | global using System.Threading.Tasks; 22 | global using System.ComponentModel.DataAnnotations; 23 | global using ConsoleAppFramework; 24 | """; 25 | 26 | var references = AppDomain.CurrentDomain.GetAssemblies() 27 | .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) 28 | .Select(x => MetadataReference.CreateFromFile(x.Location)) 29 | .Concat([ 30 | MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), // System.Console.dll 31 | MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location), // System.ComponentModel.dll 32 | MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute).Assembly.Location), // System.ComponentModel.DataAnnotations 33 | MetadataReference.CreateFromFile(typeof(System.Text.Json.JsonDocument).Assembly.Location), // System.Text.Json.dll 34 | ]); 35 | 36 | var compilation = CSharpCompilation.Create("generatortest", 37 | references: references, 38 | syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings, path: "GlobalUsings.cs")], 39 | options: new CSharpCompilationOptions(OutputKind.ConsoleApplication, allowUnsafe: true)); // .exe 40 | 41 | baseCompilation = compilation; 42 | } 43 | 44 | public static (Compilation, ImmutableArray) RunGenerator([StringSyntax("C#-test")] string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) 45 | { 46 | if (preprocessorSymbols == null) 47 | { 48 | preprocessorSymbols = new[] { "NET8_0_OR_GREATER" }; 49 | } 50 | var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13, preprocessorSymbols: preprocessorSymbols); // 13 51 | 52 | var driver = CSharpGeneratorDriver.Create(new ConsoleAppGenerator()).WithUpdatedParseOptions(parseOptions); 53 | if (options != null) 54 | { 55 | driver = (Microsoft.CodeAnalysis.CSharp.CSharpGeneratorDriver)driver.WithUpdatedAnalyzerConfigOptions(options); 56 | } 57 | 58 | // overwrite System.Console.Write/WriteLine, Environment.ExitCode to capture output 59 | var captureStaticCode = """ 60 | public static class Console 61 | { 62 | static global::System.IO.TextWriter textWriter = default!; 63 | 64 | public static void SetOut(global::System.IO.TextWriter textWriter) 65 | { 66 | Console.textWriter = textWriter; 67 | } 68 | 69 | public static void Write(object value) 70 | { 71 | textWriter.Write(value); 72 | } 73 | 74 | public static void WriteLine(object value) 75 | { 76 | textWriter.WriteLine(value); 77 | } 78 | } 79 | 80 | namespace ConsoleAppFramework 81 | { 82 | public static class Environment 83 | { 84 | public static int ExitCode { get; set; } 85 | } 86 | } 87 | """; 88 | var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(captureStaticCode, parseOptions)); 89 | 90 | driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); 91 | return (newCompilation, diagnostics); 92 | } 93 | 94 | public static (Compilation Compilation, ImmutableArray Diagnostics, string Stdout, int ExitCode) CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) 95 | { 96 | var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); 97 | 98 | using var ms = new MemoryStream(); 99 | var emitResult = compilation.Emit(ms); 100 | if (!emitResult.Success) 101 | { 102 | throw new InvalidOperationException("Emit Failed\r\n" + string.Join("\r\n", emitResult.Diagnostics.Select(x => x.ToString()))); 103 | } 104 | 105 | ms.Position = 0; 106 | 107 | var stringWriter = new StringWriter(); 108 | 109 | // load and invoke Main(args) 110 | var loadContext = new AssemblyLoadContext("source-generator", isCollectible: true); // isCollectible to support Unload 111 | var assembly = loadContext.LoadFromStream(ms); 112 | 113 | assembly.GetType("Console")!.InvokeMember("SetOut", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, [stringWriter]); 114 | 115 | assembly.EntryPoint!.Invoke(null, [args]); 116 | 117 | var exitCode = (int)assembly.GetType("ConsoleAppFramework.Environment")!.GetProperty("ExitCode")!.GetValue(null)!; 118 | 119 | loadContext.Unload(); 120 | 121 | return (compilation, diagnostics, stringWriter.ToString(), exitCode); 122 | } 123 | 124 | public static (string Key, string Reasons)[][] GetIncrementalGeneratorTrackedStepsReasons(string keyPrefixFilter, params string[] sources) 125 | { 126 | var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13); // 13 127 | var driver = CSharpGeneratorDriver.Create( 128 | [new ConsoleAppGenerator().AsSourceGenerator()], 129 | driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)) 130 | .WithUpdatedParseOptions(parseOptions); 131 | 132 | var generatorResults = sources 133 | .Select(source => 134 | { 135 | var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions)); 136 | driver = driver.RunGenerators(compilation); 137 | return driver.GetRunResult().Results[0]; 138 | }) 139 | .ToArray(); 140 | 141 | var reasons = generatorResults 142 | .Select(x => x.TrackedSteps 143 | .Where(x => x.Key.StartsWith(keyPrefixFilter) || x.Key == "SourceOutput") 144 | .Select(x => 145 | { 146 | if (x.Key == "SourceOutput") 147 | { 148 | var values = x.Value.Where(x => x.Inputs[0].Source.Name?.StartsWith(keyPrefixFilter) ?? false); 149 | return ( 150 | x.Key, 151 | Reasons: string.Join(", ", values.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) 152 | ); 153 | } 154 | else 155 | { 156 | return ( 157 | Key: x.Key.Substring(keyPrefixFilter.Length), 158 | Reasons: string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray()) 159 | ); 160 | } 161 | }) 162 | .OrderBy(x => x.Key) 163 | .ToArray()) 164 | .ToArray(); 165 | 166 | return reasons; 167 | } 168 | } 169 | 170 | public class VerifyHelper(string idPrefix) 171 | { 172 | public async Task Ok([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null) 173 | { 174 | Console.WriteLine(codeExpr!); 175 | 176 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); 177 | foreach (var item in diagnostics) 178 | { 179 | Console.WriteLine(item.ToString()); 180 | } 181 | OutputGeneratedCode(compilation); 182 | 183 | await Assert.That(diagnostics.Length).IsZero(); 184 | } 185 | 186 | public async Task Verify(int id, [StringSyntax("C#-test")] string code, string diagnosticsCodeSpan, [CallerArgumentExpression("code")] string? codeExpr = null) 187 | { 188 | Console.WriteLine(codeExpr!); 189 | 190 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); 191 | foreach (var item in diagnostics) 192 | { 193 | Console.WriteLine(item.ToString()); 194 | } 195 | OutputGeneratedCode(compilation); 196 | 197 | await Assert.That(diagnostics.Length).IsEqualTo(1); 198 | await Assert.That(diagnostics[0].Id).IsEqualTo(idPrefix + id.ToString("000")); 199 | 200 | var text = GetLocationText(diagnostics[0], compilation.SyntaxTrees); 201 | await Assert.That(text).IsEqualTo(diagnosticsCodeSpan); 202 | } 203 | 204 | public (string, string)[] Verify([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null) 205 | { 206 | Console.WriteLine(codeExpr!); 207 | 208 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); 209 | OutputGeneratedCode(compilation); 210 | return diagnostics.Select(x => (x.Id, GetLocationText(x, compilation.SyntaxTrees))).ToArray(); 211 | } 212 | 213 | // Execute and check stdout result 214 | 215 | public async Task Execute([StringSyntax("C#-test")] string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null) 216 | { 217 | Console.WriteLine(codeExpr!); 218 | 219 | var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); 220 | foreach (var item in diagnostics) 221 | { 222 | Console.WriteLine(item.ToString()); 223 | } 224 | OutputGeneratedCode(compilation); 225 | 226 | await Assert.That(stdout).IsEqualTo(expected); 227 | return exitCode; 228 | } 229 | 230 | public (string Stdout, int ExitCode) Error([StringSyntax("C#-test")] string code, string args, [CallerArgumentExpression("code")] string? codeExpr = null) 231 | { 232 | Console.WriteLine(codeExpr!); 233 | 234 | var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); 235 | foreach (var item in diagnostics) 236 | { 237 | Console.WriteLine(item.ToString()); 238 | } 239 | OutputGeneratedCode(compilation); 240 | 241 | return (stdout, exitCode); 242 | } 243 | 244 | string GetLocationText(Diagnostic diagnostic, IEnumerable syntaxTrees) 245 | { 246 | var location = diagnostic.Location; 247 | 248 | var textSpan = location.SourceSpan; 249 | var sourceTree = location.SourceTree; 250 | if (sourceTree == null) 251 | { 252 | var lineSpan = location.GetLineSpan(); 253 | if (lineSpan.Path == null) return ""; 254 | 255 | sourceTree = syntaxTrees.FirstOrDefault(x => x.FilePath == lineSpan.Path); 256 | if (sourceTree == null) return ""; 257 | } 258 | 259 | var text = sourceTree.GetText().GetSubText(textSpan).ToString(); 260 | return text; 261 | } 262 | 263 | void OutputGeneratedCode(Compilation compilation) 264 | { 265 | foreach (var syntaxTree in compilation.SyntaxTrees) 266 | { 267 | // only shows ConsoleApp.Run/Builder generated code 268 | if (!syntaxTree.FilePath.Contains("g.cs")) continue; 269 | Console.WriteLine(syntaxTree.ToString()); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace ConsoleAppFramework.GeneratorTests; 9 | 10 | public class HelpTest 11 | { 12 | VerifyHelper verifier = new VerifyHelper("CAF"); 13 | 14 | [Test] 15 | public async Task Version() 16 | { 17 | var version = GetEntryAssemblyVersion(); 18 | 19 | await verifier.Execute(code: $$""" 20 | ConsoleApp.Log = x => Console.WriteLine(x); 21 | ConsoleApp.Run(args, (int x, int y) => { }); 22 | """, 23 | args: "--version", 24 | expected: $$""" 25 | {{version}} 26 | 27 | """); 28 | // custom 29 | await verifier.Execute(code: $$""" 30 | ConsoleApp.Log = x => Console.WriteLine(x); 31 | ConsoleApp.Version = "9999.9999999abcdefg"; 32 | ConsoleApp.Run(args, (int x, int y) => { }); 33 | """, 34 | args: "--version", 35 | expected: """ 36 | 9999.9999999abcdefg 37 | 38 | """); 39 | } 40 | 41 | [Test] 42 | public async Task VersionOnBuilder() 43 | { 44 | var version = GetEntryAssemblyVersion(); 45 | 46 | await verifier.Execute(code: """ 47 | ConsoleApp.Log = x => Console.WriteLine(x); 48 | var app = ConsoleApp.Create(); 49 | app.Run(args); 50 | """, 51 | args: "--version", 52 | expected: $$""" 53 | {{version}} 54 | 55 | """); 56 | } 57 | 58 | [Test] 59 | public async Task Run() 60 | { 61 | await verifier.Execute(code: """ 62 | ConsoleApp.Log = x => Console.WriteLine(x); 63 | ConsoleApp.Run(args, (int x, int y) => { }); 64 | """, 65 | args: "--help", 66 | expected: """ 67 | Usage: [options...] [-h|--help] [--version] 68 | 69 | Options: 70 | --x [Required] 71 | --y [Required] 72 | 73 | """); 74 | } 75 | 76 | [Test] 77 | public async Task RunVoid() 78 | { 79 | await verifier.Execute(code: """ 80 | ConsoleApp.Log = x => Console.WriteLine(x); 81 | ConsoleApp.Run(args, () => { }); 82 | """, 83 | args: "--help", 84 | expected: """ 85 | Usage: [-h|--help] [--version] 86 | 87 | """); 88 | } 89 | 90 | [Test] 91 | public async Task RootOnly() 92 | { 93 | await verifier.Execute(code: """ 94 | ConsoleApp.Log = x => Console.WriteLine(x); 95 | var app = ConsoleApp.Create(); 96 | app.Add("", (int x, int y) => { }); 97 | app.Run(args); 98 | """, 99 | args: "--help", 100 | expected: """ 101 | Usage: [options...] [-h|--help] [--version] 102 | 103 | Options: 104 | --x [Required] 105 | --y [Required] 106 | 107 | """); 108 | } 109 | 110 | [Test] 111 | public async Task ListWithoutRoot() 112 | { 113 | var code = """ 114 | ConsoleApp.Log = x => Console.WriteLine(x); 115 | var app = ConsoleApp.Create(); 116 | app.Add("a", (int x, int y) => { }); 117 | app.Add("ab", (int x, int y) => { }); 118 | app.Add("a b c", (int x, int y) => { }); 119 | app.Run(args); 120 | """; 121 | await verifier.Execute(code, args: "--help", expected: """ 122 | Usage: [command] [-h|--help] [--version] 123 | 124 | Commands: 125 | a 126 | a b c 127 | ab 128 | 129 | """); 130 | } 131 | 132 | [Test] 133 | public async Task ListWithRoot() 134 | { 135 | var code = """ 136 | ConsoleApp.Log = x => Console.WriteLine(x); 137 | var app = ConsoleApp.Create(); 138 | app.Add("", (int x, int y) => { }); 139 | app.Add("a", (int x, int y) => { }); 140 | app.Add("ab", (int x, int y) => { }); 141 | app.Add("a b c", (int x, int y) => { }); 142 | app.Run(args); 143 | """; 144 | await verifier.Execute(code, args: "--help", expected: """ 145 | Usage: [command] [options...] [-h|--help] [--version] 146 | 147 | Options: 148 | --x [Required] 149 | --y [Required] 150 | 151 | Commands: 152 | a 153 | a b c 154 | ab 155 | 156 | """); 157 | } 158 | 159 | [Test] 160 | public async Task NoArgsOnRootShowsSameHelpTextAsHelpWhenParametersAreRequired() 161 | { 162 | var code = """ 163 | ConsoleApp.Log = x => Console.WriteLine(x); 164 | var app = ConsoleApp.Create(); 165 | app.Add("", (int x, int y) => { }); 166 | app.Add("a", (int x, int y) => { }); 167 | app.Add("ab", (int x, int y) => { }); 168 | app.Add("a b c", (int x, int y) => { }); 169 | app.Run(args); 170 | """; 171 | var noArgsOutput = verifier.Error(code, ""); 172 | var helpOutput = verifier.Error(code, "--help"); 173 | 174 | await Assert.That(noArgsOutput).IsEqualTo(helpOutput); 175 | } 176 | 177 | [Test] 178 | public async Task SelectLeafHelp() 179 | { 180 | var code = """ 181 | ConsoleApp.Log = x => Console.WriteLine(x); 182 | var app = ConsoleApp.Create(); 183 | app.Add("", (int x, int y) => { }); 184 | app.Add("a", (int x, int y) => { }); 185 | app.Add("ab", (int x, int y) => { }); 186 | app.Add("a b c", (int x, int y) => { }); 187 | app.Run(args); 188 | """; 189 | await verifier.Execute(code, args: "a b c --help", expected: """ 190 | Usage: a b c [options...] [-h|--help] [--version] 191 | 192 | Options: 193 | --x [Required] 194 | --y [Required] 195 | 196 | """); 197 | } 198 | 199 | [Test] 200 | public async Task Summary() 201 | { 202 | var code = """ 203 | ConsoleApp.Log = x => Console.WriteLine(x); 204 | var app = ConsoleApp.Create(); 205 | app.Add(); 206 | app.Run(args); 207 | 208 | public class MyClass 209 | { 210 | /// 211 | /// hello my world. 212 | /// 213 | /// -f|-fb, my foo is not bar. 214 | public async Task HelloWorld(string fooBar) 215 | { 216 | Console.Write("Hello World! " + fooBar); 217 | } 218 | } 219 | """; 220 | await verifier.Execute(code, args: "--help", expected: """ 221 | Usage: [command] [-h|--help] [--version] 222 | 223 | Commands: 224 | hello-world hello my world. 225 | 226 | """); 227 | 228 | await verifier.Execute(code, args: "hello-world --help", expected: """ 229 | Usage: hello-world [options...] [-h|--help] [--version] 230 | 231 | hello my world. 232 | 233 | Options: 234 | -f, -fb, --foo-bar my foo is not bar. [Required] 235 | 236 | """); 237 | } 238 | 239 | [Test] 240 | public async Task ArgumentOnly() 241 | { 242 | await verifier.Execute(code: """ 243 | ConsoleApp.Log = x => Console.WriteLine(x); 244 | ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { }); 245 | """, 246 | args: "--help", 247 | expected: """ 248 | Usage: [arguments...] [-h|--help] [--version] 249 | 250 | Arguments: 251 | [0] 252 | [1] 253 | 254 | """); 255 | } 256 | 257 | [Test] 258 | public async Task ArgumentWithParams() 259 | { 260 | await verifier.Execute(code: """ 261 | ConsoleApp.Log = x => Console.WriteLine(x); 262 | ConsoleApp.Run(args, ([Argument]int x, [Argument]int y, params string[] yyy) => { }); 263 | """, 264 | args: "--help", 265 | expected: """ 266 | Usage: [arguments...] [options...] [-h|--help] [--version] 267 | 268 | Arguments: 269 | [0] 270 | [1] 271 | 272 | Options: 273 | --yyy ... 274 | 275 | """); 276 | } 277 | 278 | // Params 279 | 280 | [Test] 281 | public async Task Nullable() 282 | { 283 | await verifier.Execute(code: """ 284 | ConsoleApp.Log = x => Console.WriteLine(x); 285 | ConsoleApp.Run(args, (int? x = null, string? y = null) => { }); 286 | """, 287 | args: "--help", 288 | expected: """ 289 | Usage: [options...] [-h|--help] [--version] 290 | 291 | Options: 292 | --x [Default: null] 293 | --y [Default: null] 294 | 295 | """); 296 | } 297 | 298 | [Test] 299 | public async Task EnumTest() 300 | { 301 | await verifier.Execute(code: """ 302 | ConsoleApp.Log = x => Console.WriteLine(x); 303 | ConsoleApp.Run(args, (Fruit myFruit = Fruit.Apple, Fruit? moreFruit = null) => { }); 304 | 305 | enum Fruit 306 | { 307 | Orange, Grape, Apple 308 | } 309 | """, 310 | args: "--help", 311 | expected: """ 312 | Usage: [options...] [-h|--help] [--version] 313 | 314 | Options: 315 | --my-fruit [Default: Apple] 316 | --more-fruit [Default: null] 317 | 318 | """); 319 | } 320 | 321 | [Test] 322 | public async Task Summary2() 323 | { 324 | var code = """ 325 | ConsoleApp.Log = x => Console.WriteLine(x); 326 | var app = ConsoleApp.Create(); 327 | app.Add(); 328 | app.Run(args); 329 | 330 | public class MyClass 331 | { 332 | /// 333 | /// hello my world. 334 | /// 335 | /// -b, my boo is not boo. 336 | /// -f|-fb, my foo, is not bar. 337 | public async Task HelloWorld([Argument]int boo, string fooBar) 338 | { 339 | Console.Write("Hello World! " + fooBar); 340 | } 341 | } 342 | """; 343 | await verifier.Execute(code, args: "hello-world --help", expected: """ 344 | Usage: hello-world [arguments...] [options...] [-h|--help] [--version] 345 | 346 | hello my world. 347 | 348 | Arguments: 349 | [0] my boo is not boo. 350 | 351 | Options: 352 | -f, -fb, --foo-bar my foo, is not bar. [Required] 353 | 354 | """); 355 | } 356 | 357 | [Test] 358 | public async Task HideDefaultValue() 359 | { 360 | var code = """ 361 | ConsoleApp.Log = x => Console.WriteLine(x); 362 | ConsoleApp.Run(args, Commands.Hello); 363 | 364 | static class Commands 365 | { 366 | /// 367 | /// Display Hello. 368 | /// 369 | /// -m, Message to show. 370 | public static void Hello([HideDefaultValue]string message = "ConsoleAppFramework") => Console.Write($"Hello, {message}"); 371 | } 372 | """; 373 | await verifier.Execute(code, args: "--help", expected: """ 374 | Usage: [options...] [-h|--help] [--version] 375 | 376 | Display Hello. 377 | 378 | Options: 379 | -m, --message Message to show. 380 | 381 | """); 382 | } 383 | 384 | [Test] 385 | public async Task GlobalOptions() 386 | { 387 | var code = """ 388 | ConsoleApp.Log = x => Console.WriteLine(x); 389 | var app = ConsoleApp.Create(); 390 | 391 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 392 | { 393 | var p = builder.AddGlobalOption("--parameter", "param global", defaultValue: 1000); 394 | var d = builder.AddGlobalOption(description: "run dry dry", name: "--dry-run"); 395 | var r = builder.AddRequiredGlobalOption("--p2|--p3", "param 2"); 396 | return (p, d, r); 397 | }); 398 | 399 | app.Add("", (int x, int y) => { }); 400 | app.Add("a", (int x, int y) => { }); 401 | app.Add("ab", (int x, int y) => { }); 402 | app.Add("a b c", (int x, int y) => { }); 403 | app.Run(args); 404 | """; 405 | 406 | await verifier.Execute(code, args: "a --help", expected: """ 407 | Usage: a [options...] [-h|--help] [--version] 408 | 409 | Options: 410 | --x [Required] 411 | --y [Required] 412 | --parameter param global [Default: 1000] 413 | --dry-run run dry dry 414 | --p2, --p3 param 2 [Required] 415 | 416 | """); 417 | } 418 | 419 | private static string GetEntryAssemblyVersion() 420 | { 421 | var version = System.Reflection.Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion; 422 | 423 | if (version == null) 424 | { 425 | return "1.0.0"; 426 | } 427 | 428 | // Trim SourceRevisionId (SourceLink feature is enabled by default when using .NET SDK 8 or later) 429 | var i = version.IndexOf('+'); 430 | if (i != -1) 431 | { 432 | version = version.Substring(0, i); 433 | } 434 | 435 | return version; 436 | } 437 | 438 | [Test] 439 | public async Task CommandAlias() 440 | { 441 | var code = """ 442 | ConsoleApp.Log = x => Console.WriteLine(x); 443 | var app = ConsoleApp.Create(); 444 | 445 | app.Add("build|b", () => { Console.Write("build ok"); }); 446 | app.Add("test|t", () => { Console.Write("test ok"); }); 447 | app.Add(); 448 | 449 | app.Run(args); 450 | 451 | public class Commands 452 | { 453 | /// Analyze the current package and report errors, but don't build object files. 454 | [Command("check|c")] 455 | public async Task Check() { Console.Write("check ok"); } 456 | 457 | /// Build this packages's and its dependencies' documenation. 458 | [Command("doc|d")] 459 | public async Task Doc() { Console.Write("doc ok"); } 460 | } 461 | """; 462 | 463 | await verifier.Execute(code, "--help", """ 464 | Usage: [command] [-h|--help] [--version] 465 | 466 | Commands: 467 | build, b 468 | check, c Analyze the current package and report errors, but don't build object files. 469 | doc, d Build this packages's and its dependencies' documenation. 470 | test, t 471 | 472 | """); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/ConsoleAppFramework/CommandHelpBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Text; 3 | 4 | namespace ConsoleAppFramework; 5 | 6 | public static class CommandHelpBuilder 7 | { 8 | public static string BuildRootHelpMessage(Command command) 9 | { 10 | return BuildHelpMessageCore(command, showCommandName: false, showCommand: false); 11 | } 12 | 13 | public static string BuildRootHelpMessage(Command[] commands) 14 | { 15 | var sb = new StringBuilder(); 16 | 17 | var rootCommand = commands.FirstOrDefault(x => x.IsRootCommand); 18 | var withoutRoot = commands.Where(x => !x.IsRootCommand).ToArray(); 19 | 20 | if (rootCommand != null && withoutRoot.Length == 0) 21 | { 22 | return BuildRootHelpMessage(commands[0]); 23 | } 24 | 25 | if (rootCommand != null) 26 | { 27 | sb.AppendLine(BuildHelpMessageCore(rootCommand, false, withoutRoot.Length != 0)); 28 | } 29 | else 30 | { 31 | sb.AppendLine("Usage: [command] [-h|--help] [--version]"); 32 | sb.AppendLine(); 33 | } 34 | 35 | if (withoutRoot.Length == 0) return sb.ToString(); 36 | 37 | var helpDefinitions = withoutRoot.OrderBy(x => x.Name).ToArray(); 38 | 39 | var list = BuildMethodListMessage(helpDefinitions, out _); 40 | sb.Append(list); 41 | 42 | return sb.ToString(); 43 | } 44 | 45 | public static string BuildCommandHelpMessage(Command command) 46 | { 47 | return BuildHelpMessageCore(command, showCommandName: command.Name != "", showCommand: false); 48 | } 49 | 50 | public static string BuildCliSchema(IEnumerable commands) 51 | { 52 | return "return new CommandHelpDefinition[] {\n" 53 | + string.Join(", \n", commands.Select(x => CreateCommandHelpDefinition(x).ToCliSchema())) 54 | + "\n};"; 55 | } 56 | 57 | static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand) 58 | { 59 | var definition = CreateCommandHelpDefinition(command); 60 | 61 | var sb = new StringBuilder(); 62 | 63 | sb.AppendLine(BuildUsageMessage(definition, showCommandName, showCommand)); 64 | 65 | if (!string.IsNullOrEmpty(definition.Description)) 66 | { 67 | sb.AppendLine(); 68 | sb.AppendLine(definition.Description); 69 | } 70 | 71 | if (definition.Options.Any()) 72 | { 73 | var hasArgument = definition.Options.Any(x => x.Index.HasValue); 74 | var hasNoHiddenOptions = definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden); 75 | 76 | if (hasArgument) 77 | { 78 | sb.AppendLine(); 79 | sb.AppendLine(BuildArgumentsMessage(definition)); 80 | } 81 | 82 | if (hasNoHiddenOptions) 83 | { 84 | sb.AppendLine(); 85 | sb.AppendLine(BuildOptionsMessage(definition)); 86 | } 87 | } 88 | 89 | return sb.ToString(); 90 | } 91 | 92 | static string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool showCommand) 93 | { 94 | var sb = new StringBuilder(); 95 | sb.Append($"Usage:"); 96 | 97 | if (showCommandName) 98 | { 99 | sb.Append($" {definition.CommandName}"); 100 | } 101 | 102 | if (showCommand) 103 | { 104 | sb.Append(" [command]"); 105 | } 106 | 107 | if (definition.Options.Any(x => x.Index.HasValue)) 108 | { 109 | sb.Append(" [arguments...]"); 110 | } 111 | 112 | if (definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden)) 113 | { 114 | sb.Append(" [options...]"); 115 | } 116 | 117 | sb.Append(" [-h|--help] [--version]"); 118 | 119 | return sb.ToString(); 120 | } 121 | 122 | static string BuildArgumentsMessage(CommandHelpDefinition definition) 123 | { 124 | var argumentsFormatted = definition.Options 125 | .Where(x => x.Index.HasValue) 126 | .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description)) 127 | .ToArray(); 128 | 129 | if (!argumentsFormatted.Any()) return string.Empty; 130 | 131 | var maxWidth = argumentsFormatted.Max(x => x.Argument.Length); 132 | 133 | var sb = new StringBuilder(); 134 | 135 | sb.AppendLine("Arguments:"); 136 | var first = true; 137 | foreach (var arg in argumentsFormatted) 138 | { 139 | if (first) 140 | { 141 | first = false; 142 | } 143 | else 144 | { 145 | sb.AppendLine(); 146 | } 147 | var padding = maxWidth - arg.Argument.Length; 148 | 149 | sb.Append(" "); 150 | sb.Append(arg.Argument); 151 | if (!string.IsNullOrEmpty(arg.Description)) 152 | { 153 | for (var i = 0; i < padding; i++) 154 | { 155 | sb.Append(' '); 156 | } 157 | 158 | sb.Append(" "); 159 | sb.Append(arg.Description); 160 | } 161 | } 162 | 163 | return sb.ToString(); 164 | } 165 | 166 | static string BuildOptionsMessage(CommandHelpDefinition definition) 167 | { 168 | var optionsFormatted = definition.Options 169 | .Where(x => !x.Index.HasValue) 170 | .Where(x => !x.IsHidden) 171 | .Select(x => (Options: string.Join(", ", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue, x.IsDefaultValueHidden)) 172 | .ToArray(); 173 | 174 | if (!optionsFormatted.Any()) return string.Empty; 175 | 176 | var maxWidth = optionsFormatted.Max(x => x.Options.Length); 177 | 178 | var sb = new StringBuilder(); 179 | 180 | sb.AppendLine("Options:"); 181 | var first = true; 182 | foreach (var opt in optionsFormatted) 183 | { 184 | if (first) 185 | { 186 | first = false; 187 | } 188 | else 189 | { 190 | sb.AppendLine(); 191 | } 192 | 193 | var options = opt.Options; 194 | var padding = maxWidth - options.Length; 195 | 196 | sb.Append(" "); 197 | sb.Append(options); 198 | for (var i = 0; i < padding; i++) 199 | { 200 | sb.Append(' '); 201 | } 202 | 203 | sb.Append(" "); 204 | sb.Append(opt.Description); 205 | 206 | // Flags are optional by default; leave them untagged. 207 | if (!opt.IsFlag) 208 | { 209 | if (opt.DefaultValue != null) 210 | { 211 | if (!opt.IsDefaultValueHidden) 212 | { 213 | sb.Append($" [Default: {opt.DefaultValue}]"); 214 | } 215 | } 216 | else if (opt.IsRequired) 217 | { 218 | sb.Append($" [Required]"); 219 | } 220 | } 221 | } 222 | 223 | return sb.ToString(); 224 | } 225 | 226 | static string BuildMethodListMessage(IEnumerable commands, out int maxWidth) 227 | { 228 | var formatted = commands 229 | .Where(x => !x.IsHidden) 230 | .Select(x => 231 | { 232 | return (Command: string.Join(", ", x.Name.Split('|')), x.Description); 233 | }) 234 | .ToArray(); 235 | maxWidth = formatted.Max(x => x.Command.Length); 236 | 237 | var sb = new StringBuilder(); 238 | 239 | sb.AppendLine("Commands:"); 240 | foreach (var item in formatted) 241 | { 242 | sb.Append(" "); 243 | sb.Append(item.Command); 244 | if (string.IsNullOrEmpty(item.Description)) 245 | { 246 | sb.AppendLine(); 247 | } 248 | else 249 | { 250 | var padding = maxWidth - item.Command.Length; 251 | for (var i = 0; i < padding; i++) 252 | { 253 | sb.Append(' '); 254 | } 255 | 256 | sb.Append(" "); 257 | sb.AppendLine(item.Description); 258 | } 259 | } 260 | 261 | return sb.ToString(); 262 | } 263 | 264 | static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) 265 | { 266 | var parameterDefinitions = new List(); 267 | 268 | foreach (var item in descriptor.Parameters) 269 | { 270 | // ignore DI params. 271 | if (!item.IsParsable) continue; 272 | 273 | // -i, -input | [default=foo]... 274 | 275 | var index = item.ArgumentIndex == -1 ? null : (int?)item.ArgumentIndex; 276 | var options = new List(); 277 | if (item.ArgumentIndex != -1) 278 | { 279 | options.Add($"[{item.ArgumentIndex}]"); 280 | } 281 | else 282 | { 283 | // aliases first 284 | foreach (var alias in item.Aliases) 285 | { 286 | options.Add(alias); 287 | } 288 | if (item.Name != null) 289 | { 290 | options.Add("--" + item.Name); 291 | } 292 | } 293 | 294 | var description = item.Description; 295 | var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; 296 | var isParams = item.IsParams; 297 | var isHidden = item.IsHidden; 298 | var isDefaultValueHidden = item.IsDefaultValueHidden; 299 | 300 | var defaultValue = default(string); 301 | if (item.HasDefaultValue) 302 | { 303 | defaultValue = item.DefaultValue == null ? "null" : item.DefaultValueToString(castValue: false, enumIncludeTypeName: false); 304 | if (isFlag) 305 | { 306 | if (item.DefaultValue is true) 307 | { 308 | // bool option with true default value is not flag. 309 | isFlag = false; 310 | } 311 | else if (item.DefaultValue is false) 312 | { 313 | // false default value should be omitted for flag. 314 | defaultValue = null; 315 | } 316 | } 317 | } 318 | 319 | var paramTypeName = item.ToTypeShortString(); 320 | parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams, isHidden, isDefaultValueHidden)); 321 | } 322 | 323 | var commandName = descriptor.Name; 324 | return new CommandHelpDefinition( 325 | commandName, 326 | parameterDefinitions.ToArray(), 327 | descriptor.Description 328 | ); 329 | } 330 | 331 | class CommandHelpDefinition 332 | { 333 | public string CommandName { get; } 334 | public CommandOptionHelpDefinition[] Options { get; } 335 | public string Description { get; } 336 | 337 | public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description) 338 | { 339 | CommandName = command; 340 | Options = options; 341 | Description = description; 342 | } 343 | 344 | public string ToCliSchema() 345 | { 346 | var sb = new StringBuilder(); 347 | sb.AppendLine($"new CommandHelpDefinition("); 348 | sb.AppendLine($" \"{EscapeString(CommandName)}\","); 349 | sb.AppendLine($" new CommandOptionHelpDefinition[]"); 350 | sb.AppendLine($" {{"); 351 | 352 | for (int i = 0; i < Options.Length; i++) 353 | { 354 | sb.Append(" "); 355 | sb.Append(Options[i].ToCliSchema()); 356 | if (i < Options.Length - 1) 357 | { 358 | sb.AppendLine(","); 359 | } 360 | else 361 | { 362 | sb.AppendLine(); 363 | } 364 | } 365 | 366 | sb.AppendLine($" }},"); 367 | sb.AppendLine($" \"{EscapeString(Description)}\""); 368 | sb.Append($")"); 369 | 370 | return sb.ToString(); 371 | } 372 | 373 | private static string EscapeString(string str) 374 | { 375 | return str.Replace("\\", "\\\\") 376 | .Replace("\"", "\\\"") 377 | .Replace("\n", "\\n") 378 | .Replace("\r", "\\r") 379 | .Replace("\t", "\\t"); 380 | } 381 | } 382 | 383 | class CommandOptionHelpDefinition 384 | { 385 | public string[] Options { get; } 386 | public string Description { get; } 387 | public string? DefaultValue { get; } 388 | public string ValueTypeName { get; } 389 | public int? Index { get; } 390 | public bool IsRequired => DefaultValue == null && !IsParams; 391 | public bool IsFlag { get; } 392 | public bool IsParams { get; } 393 | public bool IsHidden { get; } 394 | public bool IsDefaultValueHidden { get; } 395 | public string FormattedValueTypeName => "<" + ValueTypeName + ">"; 396 | 397 | public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden, bool isDefaultValueHidden) 398 | { 399 | Options = options; 400 | Description = description; 401 | ValueTypeName = valueTypeName; 402 | DefaultValue = defaultValue; 403 | Index = index; 404 | IsFlag = isFlag; 405 | IsParams = isParams; 406 | IsHidden = isHidden; 407 | IsDefaultValueHidden = isDefaultValueHidden; 408 | } 409 | 410 | public string ToCliSchema() 411 | { 412 | var optionsArray = string.Join(", ", Options.Select(o => $"\"{EscapeString(o)}\"")); 413 | var defaultValueStr = DefaultValue == null ? "null" : $"\"{EscapeString(DefaultValue)}\""; 414 | var indexStr = Index.HasValue ? Index.Value.ToString() : "null"; 415 | 416 | return $"new CommandOptionHelpDefinition(new[] {{ {optionsArray} }}, \"{EscapeString(Description)}\", \"{EscapeString(ValueTypeName)}\", {defaultValueStr}, {indexStr}, {IsFlag.ToString().ToLower()}, {IsParams.ToString().ToLower()}, {IsHidden.ToString().ToLower()}, {IsDefaultValueHidden.ToString().ToLower()})"; 417 | } 418 | 419 | private static string EscapeString(string str) 420 | { 421 | return str.Replace("\\", "\\\\") 422 | .Replace("\"", "\\\"") 423 | .Replace("\n", "\\n") 424 | .Replace("\r", "\\r") 425 | .Replace("\t", "\\t"); 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleAppFramework.GeneratorTests; 2 | 3 | public class DiagnosticsTest 4 | { 5 | VerifyHelper verifier = new VerifyHelper("CAF"); 6 | 7 | [Test] 8 | public async Task ArgumentCount() 9 | { 10 | await verifier.Verify(1, "ConsoleApp.Run(args);", "ConsoleApp.Run(args)"); 11 | await verifier.Verify(1, "ConsoleApp.Run();", "ConsoleApp.Run()"); 12 | await verifier.Verify(1, "ConsoleApp.Run(args, (int x, int y) => { }, 1000);", "ConsoleApp.Run(args, (int x, int y) => { }, 1000)"); 13 | } 14 | 15 | [Test] 16 | public async Task InvalidReturnTypeFromLambda() 17 | { 18 | await verifier.Verify(2, "ConsoleApp.Run(args, string (int x, int y) => { return \"foo\"; })", "string"); 19 | await verifier.Verify(2, "ConsoleApp.Run(args, int? (int x, int y) => { return -1; })", "int?"); 20 | await verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task"); 21 | await verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task"); 22 | await verifier.Verify(2, "ConsoleApp.Run(args, async Task (int x, int y) => { return \"foo\"; })", "Task"); 23 | await verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { })", "ValueTask"); 24 | await verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask"); 25 | await verifier.Ok("ConsoleApp.Run(args, (int x, int y) => { })"); 26 | await verifier.Ok("ConsoleApp.Run(args, void (int x, int y) => { })"); 27 | await verifier.Ok("ConsoleApp.Run(args, int (int x, int y) => { })"); 28 | await verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })"); 29 | await verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })"); 30 | } 31 | 32 | [Test] 33 | public async Task InvalidReturnTypeFromMethodReference() 34 | { 35 | await verifier.Verify(3, "ConsoleApp.Run(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float"); 36 | await verifier.Verify(3, "ConsoleApp.Run(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task"); 37 | await verifier.Ok("ConsoleApp.Run(args, Run); void Run(int x, int y) { };"); 38 | await verifier.Ok("ConsoleApp.Run(args, Run); static void Run(int x, int y) { };"); 39 | await verifier.Ok("ConsoleApp.Run(args, Run); int Run(int x, int y) => -1;"); 40 | await verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) { };"); 41 | await verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) => -1;"); 42 | } 43 | 44 | [Test] 45 | public async Task RunAsyncValidation() 46 | { 47 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, string (int x, int y) => { return \"foo\"; })", "string"); 48 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, int? (int x, int y) => { return -1; })", "int?"); 49 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task"); 50 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task"); 51 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async Task (int x, int y) => { return \"foo\"; })", "Task"); 52 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { })", "ValueTask"); 53 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask"); 54 | await verifier.Ok("ConsoleApp.RunAsync(args, (int x, int y) => { })"); 55 | await verifier.Ok("ConsoleApp.RunAsync(args, void (int x, int y) => { })"); 56 | await verifier.Ok("ConsoleApp.RunAsync(args, int (int x, int y) => { })"); 57 | await verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); 58 | await verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); 59 | 60 | await verifier.Verify(3, "ConsoleApp.RunAsync(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float"); 61 | await verifier.Verify(3, "ConsoleApp.RunAsync(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task"); 62 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); void Run(int x, int y) { };"); 63 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); static void Run(int x, int y) { };"); 64 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); int Run(int x, int y) => -1;"); 65 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) { };"); 66 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) => -1;"); 67 | } 68 | 69 | // v5.7.7 supports non-first argument parameters 70 | //[Fact] 71 | //public async Task Argument() 72 | //{ 73 | // await verifier.Verify(4, "ConsoleApp.Run(args, (int x, [Argument]int y) => { })", "[Argument]int y"); 74 | // await verifier.Verify(4, "ConsoleApp.Run(args, ([Argument]int x, int y, [Argument]int z) => { })", "[Argument]int z"); 75 | // await verifier.Verify(4, "ConsoleApp.Run(args, Run); void Run(int x, [Argument]int y) { };", "[Argument]int y"); 76 | 77 | // await verifier.Ok("ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { })"); 78 | // await verifier.Ok("ConsoleApp.Run(args, Run); void Run([Argument]int x, [Argument]int y) { };"); 79 | //} 80 | 81 | [Test] 82 | public async Task FunctionPointerValidation() 83 | { 84 | await verifier.Verify(5, "unsafe { ConsoleApp.Run(args, &Run2); static void Run2([Range(1, 10)]int x, int y) { }; }", "[Range(1, 10)]int x"); 85 | 86 | await verifier.Ok("unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, int y) { }; }"); 87 | } 88 | 89 | [Test] 90 | public async Task BuilderAddConstCommandName() 91 | { 92 | await verifier.Verify(6, """ 93 | var builder = ConsoleApp.Create(); 94 | var baz = "foo"; 95 | builder.Add(baz, (int x, int y) => { } ); 96 | """, "baz"); 97 | 98 | await verifier.Ok(""" 99 | var builder = ConsoleApp.Create(); 100 | builder.Add("foo", (int x, int y) => { } ); 101 | builder.Run(args); 102 | """); 103 | } 104 | 105 | [Test] 106 | public async Task DuplicateCommandName() 107 | { 108 | await verifier.Verify(7, """ 109 | var builder = ConsoleApp.Create(); 110 | builder.Add("foo", (int x, int y) => { } ); 111 | builder.Add("foo", (int x, int y) => { } ); 112 | """, "\"foo\""); 113 | } 114 | 115 | [Test] 116 | public async Task DuplicateCommandNameClass() 117 | { 118 | await verifier.Verify(7, """ 119 | var builder = ConsoleApp.Create(); 120 | builder.Add(); 121 | 122 | public class MyClass 123 | { 124 | public async Task Do() 125 | { 126 | Console.Write("yeah:"); 127 | } 128 | 129 | public async Task Do(int i) 130 | { 131 | Console.Write("yeah:"); 132 | } 133 | } 134 | """, "builder.Add()"); 135 | 136 | await verifier.Verify(7, """ 137 | var builder = ConsoleApp.Create(); 138 | builder.Add("do", (int x, int y) => { } ); 139 | builder.Add(); 140 | builder.Run(args); 141 | 142 | public class MyClass 143 | { 144 | public async Task Do() 145 | { 146 | Console.Write("yeah:"); 147 | } 148 | } 149 | """, "builder.Add()"); 150 | } 151 | 152 | [Test] 153 | public async Task AddInLoop() 154 | { 155 | var myClass = """ 156 | public class MyClass 157 | { 158 | public async Task Do() 159 | { 160 | Console.Write("yeah:"); 161 | } 162 | } 163 | """; 164 | await verifier.Verify(8, $$""" 165 | var builder = ConsoleApp.Create(); 166 | while (true) 167 | { 168 | builder.Add(); 169 | } 170 | 171 | {{myClass}} 172 | """, "builder.Add()"); 173 | 174 | await verifier.Verify(8, $$""" 175 | var builder = ConsoleApp.Create(); 176 | for (int i = 0; i < 10; i++) 177 | { 178 | builder.Add(); 179 | } 180 | 181 | {{myClass}} 182 | """, "builder.Add()"); 183 | 184 | await verifier.Verify(8, $$""" 185 | var builder = ConsoleApp.Create(); 186 | do 187 | { 188 | builder.Add(); 189 | } while(true); 190 | 191 | {{myClass}} 192 | """, "builder.Add()"); 193 | 194 | await verifier.Verify(8, $$""" 195 | var builder = ConsoleApp.Create(); 196 | foreach (var item in new[]{1,2,3}) 197 | { 198 | builder.Add(); 199 | } 200 | 201 | {{myClass}} 202 | """, "builder.Add()"); 203 | } 204 | 205 | [Test] 206 | public async Task ErrorInBuilderAPI() 207 | { 208 | await verifier.Verify(3, $$""" 209 | var builder = ConsoleApp.Create(); 210 | builder.Add(); 211 | 212 | public class MyClass 213 | { 214 | public string Do() 215 | { 216 | Console.Write("yeah:"); 217 | return "foo"; 218 | } 219 | } 220 | """, "string"); 221 | 222 | await verifier.Verify(3, $$""" 223 | var builder = ConsoleApp.Create(); 224 | builder.Add(); 225 | 226 | public class MyClass 227 | { 228 | public async Task Do() 229 | { 230 | Console.Write("yeah:"); 231 | return "foo"; 232 | } 233 | } 234 | """, "Task"); 235 | 236 | await verifier.Verify(2, $$""" 237 | var builder = ConsoleApp.Create(); 238 | builder.Add("foo", string (int x, int y) => { return "foo"; }); 239 | """, "string"); 240 | 241 | await verifier.Verify(2, $$""" 242 | var builder = ConsoleApp.Create(); 243 | builder.Add("foo", async Task (int x, int y) => { return "foo"; }); 244 | """, "Task"); 245 | } 246 | 247 | 248 | 249 | [Test] 250 | public async Task RunAndFilter() 251 | { 252 | await verifier.Verify(9, """ 253 | ConsoleApp.Run(args, Hello); 254 | 255 | [ConsoleAppFilter] 256 | void Hello() 257 | { 258 | } 259 | 260 | public class NopFilter(ConsoleAppFilter next) 261 | : ConsoleAppFilter(next) 262 | { 263 | public override Task InvokeAsync(CancellationToken cancellationToken) 264 | { 265 | return Next.InvokeAsync(cancellationToken); 266 | } 267 | } 268 | """, "ConsoleApp.Run(args, Hello)"); 269 | } 270 | 271 | [Test] 272 | public async Task MultiConstructorFilter() 273 | { 274 | await verifier.Verify(10, """ 275 | var app = ConsoleApp.Create(); 276 | app.UseFilter(); 277 | app.Add("", Hello); 278 | app.Run(args); 279 | 280 | void Hello() 281 | { 282 | } 283 | 284 | internal class NopFilter : ConsoleAppFilter 285 | { 286 | public NopFilter(ConsoleAppFilter next) 287 | :base(next) 288 | { 289 | } 290 | 291 | public NopFilter(string x, ConsoleAppFilter next) 292 | :base(next) 293 | { 294 | } 295 | 296 | public override Task InvokeAsync(CancellationToken cancellationToken) 297 | { 298 | return Next.InvokeAsync(cancellationToken); 299 | } 300 | } 301 | """, "NopFilter"); 302 | 303 | await verifier.Verify(10, """ 304 | var app = ConsoleApp.Create(); 305 | app.Add(); 306 | app.Run(args); 307 | 308 | [ConsoleAppFilter] 309 | public class Foo 310 | { 311 | public async Task Hello() 312 | { 313 | } 314 | } 315 | 316 | internal class NopFilter : ConsoleAppFilter 317 | { 318 | public NopFilter(ConsoleAppFilter next) 319 | :base(next) 320 | { 321 | } 322 | 323 | public NopFilter(string x, ConsoleAppFilter next) 324 | :base(next) 325 | { 326 | } 327 | 328 | public override Task InvokeAsync(CancellationToken cancellationToken) 329 | { 330 | return Next.InvokeAsync(cancellationToken); 331 | } 332 | } 333 | """, "ConsoleAppFilter"); 334 | 335 | await verifier.Verify(10, """ 336 | var app = ConsoleApp.Create(); 337 | app.Add(); 338 | app.Run(args); 339 | 340 | public class Foo 341 | { 342 | [ConsoleAppFilter] 343 | public async Task Hello() 344 | { 345 | } 346 | } 347 | 348 | internal class NopFilter : ConsoleAppFilter 349 | { 350 | public NopFilter(ConsoleAppFilter next) 351 | :base(next) 352 | { 353 | } 354 | 355 | public NopFilter(string x, ConsoleAppFilter next) 356 | :base(next) 357 | { 358 | } 359 | 360 | public override Task InvokeAsync(CancellationToken cancellationToken) 361 | { 362 | return Next.InvokeAsync(cancellationToken); 363 | } 364 | } 365 | """, "ConsoleAppFilter"); 366 | } 367 | 368 | 369 | [Test] 370 | public async Task MultipleCtorClass() 371 | { 372 | await verifier.Verify(11, """ 373 | var app = ConsoleApp.Create(); 374 | app.Add(); 375 | app.Run(args); 376 | 377 | public class Foo 378 | { 379 | public Foo() { } 380 | public Foo(int x) { } 381 | 382 | public async Task Hello() 383 | { 384 | } 385 | } 386 | """, "app.Add()"); 387 | } 388 | 389 | [Test] 390 | public async Task PublicMethods() 391 | { 392 | await verifier.Verify(12, """ 393 | var app = ConsoleApp.Create(); 394 | app.Add(); 395 | app.Run(args); 396 | 397 | public class Foo 398 | { 399 | public Foo() { } 400 | public Foo(int x) { } 401 | 402 | private void Hello() 403 | { 404 | } 405 | } 406 | """, "app.Add()"); 407 | } 408 | 409 | [Test] 410 | public async Task AbstractNotAllow() 411 | { 412 | await verifier.Verify(13, """ 413 | var app = ConsoleApp.Create(); 414 | app.Add(); 415 | app.Run(args); 416 | 417 | public abstract class Foo 418 | { 419 | public async Task Hello() 420 | { 421 | } 422 | } 423 | """, "app.Add()"); 424 | 425 | await verifier.Verify(13, """ 426 | var app = ConsoleApp.Create(); 427 | app.Add(); 428 | app.Run(args); 429 | 430 | public interface IFoo 431 | { 432 | void Hello(); 433 | } 434 | """, "app.Add()"); 435 | } 436 | 437 | [Test] 438 | public async Task DocCommentName() 439 | { 440 | await verifier.Verify(15, """ 441 | var app = ConsoleApp.Create(); 442 | app.Add(); 443 | app.Run(args); 444 | 445 | public class Foo 446 | { 447 | /// foobarbaz! 448 | [Command("Error1")] 449 | public async Task Bar(string msg) 450 | { 451 | Console.WriteLine(msg); 452 | } 453 | } 454 | 455 | """, "Bar"); 456 | 457 | } 458 | 459 | [Test] 460 | public async Task AsyncVoid() 461 | { 462 | await verifier.Verify(16, """ 463 | var app = ConsoleApp.Create(); 464 | app.Add(); 465 | app.Run(args); 466 | 467 | public class MyCommands2 468 | { 469 | public async void Foo() 470 | { 471 | await Task.Yield(); 472 | } 473 | } 474 | 475 | """, "async"); 476 | } 477 | 478 | [Test] 479 | public async Task GlobalOptionsDuplicate() 480 | { 481 | await verifier.Verify(17, """ 482 | var app = ConsoleApp.Create(); 483 | 484 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 485 | { 486 | return new object(); 487 | }); 488 | 489 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 490 | { 491 | return new object(); 492 | }); 493 | 494 | app.Run(args); 495 | """, "app.ConfigureGlobalOptions"); 496 | } 497 | 498 | [Test] 499 | public async Task GlobalOptionsInvalidType() 500 | { 501 | await verifier.Verify(18, """ 502 | var app = ConsoleApp.Create(); 503 | 504 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => 505 | { 506 | builder.AddGlobalOption("foo"); 507 | return new object(); 508 | }); 509 | 510 | app.Run(args); 511 | """, "builder.AddGlobalOption(\"foo\")"); 512 | } 513 | 514 | } 515 | --------------------------------------------------------------------------------