diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c9e08409 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,38 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# 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) +spelling_exclusion_path = ./exclusion.dic + +[*.cs] +indent_size = 4 +charset = utf-8-bom +end_of_line = unset + +# Solution files +[*.{sln,slnx}] +end_of_line = unset + +# MSBuild project files +[*.{csproj,props,targets}] +end_of_line = unset + +# Xml config files +[*.{ruleset,config,nuspec,resx,runsettings,DotSettings}] +end_of_line = unset + +# C# code style settings +[*.{cs}] +dotnet_diagnostic.IDE0044.severity = none # IDE0044: Make field readonly + +# https://stackoverflow.com/questions/79195382/how-to-disable-fading-unused-methods-in-visual-studio-2022-17-12-0 +dotnet_diagnostic.IDE0051.severity = none # IDE0051: Remove unused private member +dotnet_diagnostic.IDE0130.severity = none # IDE0130: Namespace does not match folder structure diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 00aea0a7..63b36f3d 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -5,3 +5,8 @@ updates: directory: "/" schedule: interval: "weekly" # Check for updates to GitHub Actions every week + ignore: + # I just want update action when major/minor version is updated. patch updates are too noisy. + - dependency-name: '*' + update-types: + - version-update:semver-patch diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yaml similarity index 72% rename from .github/workflows/build-debug.yml rename to .github/workflows/build-debug.yaml index 1f1949b2..9d5952bb 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yaml @@ -11,10 +11,12 @@ on: jobs: build-dotnet: - runs-on: ubuntu-latest + permissions: + contents: read + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: Cysharp/Actions/.github/actions/checkout@main - uses: Cysharp/Actions/.github/actions/setup-dotnet@main - run: dotnet build -c Debug - run: dotnet test -c Debug --no-build diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yaml similarity index 87% rename from .github/workflows/build-release.yml rename to .github/workflows/build-release.yaml index 3017b072..e1d8984c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yaml @@ -14,10 +14,12 @@ on: jobs: build-dotnet: - runs-on: ubuntu-latest + permissions: + contents: read + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: Cysharp/Actions/.github/actions/checkout@main - uses: Cysharp/Actions/.github/actions/setup-dotnet@main # pack nuget - run: dotnet build -c Release -p:Version=${{ inputs.tag }} @@ -32,6 +34,8 @@ jobs: # release create-release: needs: [build-dotnet] + permissions: + contents: write uses: Cysharp/Actions/.github/workflows/create-release.yaml@main with: commit-id: ${{ github.sha }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yaml similarity index 66% rename from .github/workflows/stale.yml rename to .github/workflows/stale.yaml index b480c3e3..c333a85b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yaml @@ -7,4 +7,8 @@ on: jobs: stale: + permissions: + contents: read + pull-requests: write + issues: write uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 124388f5..b36345cd 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -8,14 +8,18 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AAD2D900-C305-4449-A9FC-6C7696FFEDFA}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6DF6534A-0F9D-44A4-BF89-AE1F3B243914}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore + .editorconfig = .editorconfig .gitignore = .gitignore - .circleci\config.yml = .circleci\config.yml Directory.Build.props = Directory.Build.props ReadMe.md = ReadMe.md + exclusion.dic = exclusion.dic EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework", "src\ConsoleAppFramework\ConsoleAppFramework.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" diff --git a/ReadMe.md b/ReadMe.md index 8c12d031..5b7c8d99 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,8 +1,8 @@ -ConsoleAppFramework +ConsoleAppFramework === [![GitHub Actions](https://github.com/Cysharp/ConsoleAppFramework/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ConsoleAppFramework/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ConsoleAppFramework.svg)](https://github.com/Cysharp/ConsoleAppFramework/releases) -ConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator; achieves exceptionally high performance, fastest start-up time(with NativeAOT) and minimal binary size. Leveraging the latest features of .NET 8 and C# 12 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility. +ConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator; achieves exceptionally high performance, fastest start-up time(with NativeAOT) and minimal binary size. Leveraging the latest features of .NET 8 and C# 13 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility. ![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/db4bf599-9fe0-4ce4-801f-0003f44d5628) > Set `RunStrategy=ColdStart WarmupCount=0` to calculate the cold start benchmark, which is suitable for CLI application. @@ -147,6 +147,7 @@ ConsoleAppFramework offers a rich set of features as a framework. The Source Gen * High performance value parsing via `ISpanParsable` * Parsing of params arrays * Parsing of JSON arguments +* Double-dash escape arguments * Help(`-h|--help`) option builder * Default show version(`--version`) option @@ -154,7 +155,7 @@ As you can see from the generated output, the help display is also fast. In typi Getting Started -- -This library is distributed via NuGet, minimal requirement is .NET 8 and C# 12. +This library is distributed via NuGet, minimal requirement is .NET 8 and C# 13. > dotnet add package [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework) @@ -168,6 +169,13 @@ using ConsoleAppFramework; ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}")); ``` +> When using .NET 8, you need to explicitly set LangVersion to 13 or above. +> ```xml +> +> net8.0 +> 13 +> + > The latest Visual Studio changed the execution timing of Source Generators to either during save or at compile time. If you encounter unexpected behavior, try compiling once or change the option to "Automatic" under TextEditor -> C# -> Advanced -> Source Generators. You can execute command like `sampletool --name "foo"`. @@ -605,6 +613,25 @@ By setting this attribute on a parameter, the custom parser will be called when ConsoleApp.Run(args, ([Vector3Parser] Vector3 position) => Console.WriteLine(position)); ``` +### Double-dash escaping + +Arguments after double-dash (`--`) can be received as escaped arguments without being parsed. This is useful when creating commands like `dotnet run`. +```csharp +// dotnet run --project foo.csproj -- --foo 100 --bar bazbaz +var app = ConsoleApp.Create(); +app.Add("run", (string project, ConsoleAppContext context) => +{ + // run --project foo.csproj -- --foo 100 --bar bazbaz + Console.WriteLine(string.Join(" ", context.Arguments)); + // --project foo.csproj + Console.WriteLine(string.Join(" ", context.CommandArguments!)); + // --foo 100 --bar bazbaz + Console.WriteLine(string.Join(" ", context.EscapedArguments!)); +}); +app.Run(args); +``` +You can get the escaped arguments using `ConsoleAppContext.EscapedArguments`. From `ConsoleAppContext`, you can also get `Arguments` which contains all arguments passed to `Run/RunAsync`, and `CommandArguments` which contains the arguments used for command execution. + ### Syntax Parsing Policy and Performance While there are some standards for command-line arguments, such as UNIX tools and POSIX, there is no absolute specification. The [Command-line syntax overview for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax) provides an explanation of the specifications adopted by System.CommandLine. However, ConsoleAppFramework, while referring to these specifications to some extent, does not necessarily aim to fully comply with them. @@ -670,7 +697,14 @@ The field secondArg must be between 0 and 2. By default, the ExitCode is set to 1 in this case. -Filter(Middleware) Pipline / ConsoleAppContext +Hide command/parameter help +--- +`ConsoleAppFramework` supports `HiddenAttribute` which is used to hide specific help for a command/parameter. + +- When`HiddenAttribute` is set to command, it hides command from command list. +- When`HiddenAttribute` is set to parameter, it hides parameter from command help. + +Filter(Middleware) Pipeline / ConsoleAppContext --- Filters are provided as a mechanism to hook into the execution before and after. To use filters, define an `internal class` that implements `ConsoleAppFilter`. @@ -903,7 +937,7 @@ app.Add("", ([FromServices] MyService service, int x, int y) => Console.WriteLin app.Run(args); ``` -When passing to a lambda expression or method, the `[FromServices]` attribute is used to distinguish it from command parameters. When passing a class, Constructor Injection can be used, resulting in a simpler appearance. +When passing to a lambda expression or method, the `[FromServices]` attribute is used to distinguish it from command parameters. When passing a class, Constructor Injection can be used, resulting in a simpler appearance. Lambda, method, constructor, filter, etc, all DI supported parameter also supports `[FromKeyedServices]`. Let's try injecting a logger and enabling output to a file. The libraries used are Microsoft.Extensions.Logging and [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) (a high-performance logger built on top of MS.E.Logging). If you are referencing `Microsoft.Extensions.Logging`, you can call `ConfigureLogging` from `ConsoleAppBuilder`. @@ -966,6 +1000,9 @@ internal sealed class ReplaceLogFilter(ConsoleAppFilter next, ILogger l } ``` +> I don't recommend using `ConsoleApp.Log` and `ConsoleApp.LogError` directly as an application logging method, as they are intended to be used as output destinations for internal framework output. +> For error handling, it would be better to define your own custom filters for error handling, which would allow you to record more details when handling errors. + DI can also be effectively used when reading application configuration from `appsettings.json`. For example, suppose you have the following JSON file. ```json @@ -1022,7 +1059,7 @@ When `Microsoft.Extensions.Configuration` is imported, `ConfigureEmptyConfigurat Furthermore, overloads of `Action configure` and `Action configure` are added to `ConfigureServices` and `ConfigureLogging`, allowing you to retrieve the Configuration when executing the delegate. -without Hosting dependency, I've prefere these import packages. +without Hosting dependency, I've preferred these import packages. ```xml @@ -1106,7 +1143,7 @@ ConsoleApp.ServiceProvider = scope.ServiceProvider; * `app.AddAllCommandType` -> `NotSupported`(use `Add` manually) * `[Option(int index)]` -> `[Argument]` * `[Option(string shortName, string description)]` -> `Xml Document Comment` -* `ConsoleAppFilter.Order` -> `NotSupported`(global -> class -> method declrative order) +* `ConsoleAppFilter.Order` -> `NotSupported`(global -> class -> method declarative order) * `ConsoleAppOptions.GlobalFilters` -> `app.UseFilter` * `ConsoleAppBase` -> inject `ConsoleAppContext`, `CancellationToken` to method diff --git a/exclusion.dic b/exclusion.dic new file mode 100644 index 00000000..4d216395 --- /dev/null +++ b/exclusion.dic @@ -0,0 +1,41 @@ +abcd +abcde +abcdefg +aiueo +appsettings +args +authed +awaitable +Awaiter +Binded +Clipr +Cysharp +Decr +dest +Equatable +fooa +foobarbaz +generatortest +hoge +ignorecase +Impl +Incr +Kabayaki +Kokuban +Lamda +Moge +nomsg +nomunomu +Numerics +Parsable +posix +saas +Spectre +stackalloc +stdout +Tacommands +tako +takoyaki +Withargs +Yaki +Zeroargs diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index 0a7fc0ca..c78de1c8 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -1,23 +1,15 @@ -// This benchmark project is based on CliFx.Benchmarks. +// This benchmark project is based on CliFx.Benchmarks. // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Engines; using BenchmarkDotNet.Order; using CliFx; using Cocona.Benchmark.External.Commands; -using CommandLine; using ConsoleAppFramework; -using PowerArgs; using Spectre.Console.Cli; -using System.ComponentModel.DataAnnotations.Schema; -using BenchmarkDotNet.Columns; namespace Cocona.Benchmark.External; -// use ColdStart strategy to measure startup time evaluation -[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 1, invocationCount: 1)] -[MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class Benchmark { @@ -124,4 +116,4 @@ public void ExecuteSpectreConsoleCli() // app.Add("", ConsoleAppFrameworkCommand.Execute); // app.Run(Arguments); //} -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 2acf4fe7..8cc59763 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -1,40 +1,41 @@  - - Exe - net8.0 - enable - annotations - true - false - Debug;Release - + + Exe + net8.0 + enable + annotations + true + false + Debug;Release + - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - Analyzer - false - - + + + Analyzer + false + + diff --git a/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs index 6d73e063..d3c48dae 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs @@ -1,4 +1,4 @@ -using CliFx.Attributes; +using CliFx.Attributes; using CliFx.Infrastructure; namespace Cocona.Benchmark.External.Commands; @@ -14,6 +14,6 @@ public class CliFxCommand : CliFx.ICommand [CommandOption("bool", 'b')] public bool BoolOption { get; set; } - + public ValueTask ExecuteAsync(IConsole console) => ValueTask.CompletedTask; } diff --git a/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs index 3a5b9ceb..34872d3b 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs @@ -1,4 +1,4 @@ -using clipr; +using clipr; namespace Cocona.Benchmark.External.Commands; @@ -16,4 +16,4 @@ public class CliprCommand public void Execute() { } -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs index 4e299f03..37f39977 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs @@ -1,4 +1,4 @@ -namespace Cocona.Benchmark.External.Commands; +namespace Cocona.Benchmark.External.Commands; public class CoconaCommand { @@ -11,4 +11,4 @@ public void Execute( bool boolOption) { } -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs index b15dc2f2..2ab1d58a 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs @@ -1,4 +1,4 @@ -namespace Cocona.Benchmark.External.Commands; +namespace Cocona.Benchmark.External.Commands; public class CommandLineParserCommand { @@ -14,4 +14,4 @@ public class CommandLineParserCommand public void Execute() { } -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs index 8a5fb9af..ad7adcbd 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -1,4 +1,4 @@ -//using ConsoleAppFramework; +//using ConsoleAppFramework; //namespace Cocona.Benchmark.External.Commands; @@ -51,4 +51,4 @@ public static void Execute(string? str, int intOption, bool boolOption, Cancella // { // return Next.InvokeAsync(cancellationToken); // } -//} \ No newline at end of file +//} diff --git a/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs index d9ff0d74..ae6b2b4c 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs @@ -1,4 +1,4 @@ -namespace Cocona.Benchmark.External.Commands; +namespace Cocona.Benchmark.External.Commands; public class McMasterCommand { @@ -12,4 +12,4 @@ public class McMasterCommand public bool BoolOption { get; set; } public int OnExecute() => 0; -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs index 33d14c29..c9d0e72e 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs @@ -1,4 +1,4 @@ -using PowerArgs; +using PowerArgs; namespace Cocona.Benchmark.External.Commands; @@ -16,4 +16,4 @@ namespace Cocona.Benchmark.External.Commands; // public void Main() // { // } -//} \ No newline at end of file +//} diff --git a/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs index 0a8d4c28..411ecc25 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs @@ -21,4 +21,4 @@ public override int Execute(CommandContext context, Settings settings) { return 0; } -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs index ca5a114d..3a7f72b5 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs @@ -1,5 +1,5 @@ -using System.CommandLine; -using System.CommandLine.Invocation; +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; namespace Cocona.Benchmark.External.Commands; @@ -11,43 +11,37 @@ public static int Execute(string[] args) { var command = new RootCommand { - new Option(new[] {"--str", "-s"}) - { - Argument = new Argument() - }, - new Option(new[] {"--int", "-i"}) - { - Argument = new Argument() - }, - new Option(new[] {"--bool", "-b"}) - { - Argument = new Argument() - } + new Option("--str", ["-s"]), + new Option("--int", ["-i"]), + new Option("--bool", ["-b"]), }; - command.Handler = CommandHandler.Create(ExecuteHandler); - return command.Invoke(args); + command.SetAction(parseResult => + { + var handler = CommandHandler.Create(ExecuteHandler); + return handler.InvokeAsync(parseResult); + }); + + ParseResult parseResult = command.Parse(args); + return parseResult.Invoke(); } public static Task ExecuteAsync(string[] args) { var command = new RootCommand { - new Option(new[] {"--str", "-s"}) - { - Argument = new Argument() - }, - new Option(new[] {"--int", "-i"}) - { - Argument = new Argument() - }, - new Option(new[] {"--bool", "-b"}) - { - Argument = new Argument() - } + new Option("--str", ["-s"]), + new Option("--int", ["-i"]), + new Option("--bool", ["-b"]), }; - command.Handler = CommandHandler.Create(ExecuteHandler); - return command.InvokeAsync(args); + command.SetAction((parseResult, cancellationToken) => + { + var handler = CommandHandler.Create(ExecuteHandler); + return handler.InvokeAsync(parseResult); + }); + + ParseResult parseResult = command.Parse(args); + return parseResult.InvokeAsync(); } -} \ No newline at end of file +} diff --git a/sandbox/CliFrameworkBenchmark/Program.cs b/sandbox/CliFrameworkBenchmark/Program.cs index f71dd38e..a75f8048 100644 --- a/sandbox/CliFrameworkBenchmark/Program.cs +++ b/sandbox/CliFrameworkBenchmark/Program.cs @@ -1,9 +1,13 @@ -// This benchmark project is based on CliFx.Benchmarks. +// This benchmark project is based on CliFx.Benchmarks. // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.CsProj; using Perfolizer.Horology; namespace Cocona.Benchmark.External; @@ -12,6 +16,22 @@ class Program { static void Main(string[] args) { - BenchmarkRunner.Run(DefaultConfig.Instance.WithSummaryStyle(SummaryStyle.Default.WithTimeUnit(TimeUnit.Millisecond))); + var config = DefaultConfig.Instance + .WithSummaryStyle(SummaryStyle.Default + .WithTimeUnit(TimeUnit.Millisecond)); + + config.AddDiagnoser(MemoryDiagnoser.Default); + config.AddDiagnoser(new ThreadingDiagnoser(new ThreadingDiagnoserConfig(displayLockContentionWhenZero: false, displayCompletedWorkItemCountWhenZero: false))); + + config.AddJob(Job.Default + .WithStrategy(RunStrategy.ColdStart) + .WithLaunchCount(1) + .WithWarmupCount(0) + .WithIterationCount(1) + .WithInvocationCount(1) + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .DontEnforcePowerPlan()); + + BenchmarkRunner.Run(config, args); } } diff --git a/sandbox/CliFrameworkBenchmark/Properties/launchSettings.json b/sandbox/CliFrameworkBenchmark/Properties/launchSettings.json new file mode 100644 index 00000000..f56b87d6 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Default": { + "commandName": "Project", + "commandLineArgs": "" + }, + "Measure": { + "commandName": "Project", + "commandLineArgs": "--launchCount 20" + } + } +} diff --git a/sandbox/CliFrameworkBenchmark/README.md b/sandbox/CliFrameworkBenchmark/README.md index 9c8ed355..7fe449f2 100644 --- a/sandbox/CliFrameworkBenchmark/README.md +++ b/sandbox/CliFrameworkBenchmark/README.md @@ -2,4 +2,4 @@ https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ -https://github.com/mayuki/Cocona/tree/master/perf/Cocona.Benchmark.External \ No newline at end of file +https://github.com/mayuki/Cocona/tree/master/perf/Cocona.Benchmark.External diff --git a/sandbox/FilterShareProject/Class1.cs b/sandbox/FilterShareProject/Class1.cs index 8a619b7d..dc62a25a 100644 --- a/sandbox/FilterShareProject/Class1.cs +++ b/sandbox/FilterShareProject/Class1.cs @@ -17,4 +17,4 @@ public void Execute(int x) { Console.WriteLine("Hello?"); } -} \ No newline at end of file +} diff --git a/sandbox/FilterShareProject/FilterShareProject.csproj b/sandbox/FilterShareProject/FilterShareProject.csproj index 181c71e4..e7bdf612 100644 --- a/sandbox/FilterShareProject/FilterShareProject.csproj +++ b/sandbox/FilterShareProject/FilterShareProject.csproj @@ -3,9 +3,9 @@ net8.0 enable - false - enable - Debug;Release + false + enable + Debug;Release diff --git a/sandbox/GeneratorSandbox/Filters.cs b/sandbox/GeneratorSandbox/Filters.cs index a05c887d..4917f95b 100644 --- a/sandbox/GeneratorSandbox/Filters.cs +++ b/sandbox/GeneratorSandbox/Filters.cs @@ -54,7 +54,7 @@ async Task GetUserIdAsync() } } -record class ApplicationContext(Guid RequiestId, int UserId); +record class ApplicationContext(Guid RequestId, int UserId); internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { @@ -119,4 +119,4 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo // await using var scope = serviceProvider.CreateAsyncScope(); // await Next.InvokeAsync(context, cancellationToken); // } -//} \ No newline at end of file +//} diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 89c2ed5a..e892029e 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -1,40 +1,43 @@ - - - - Exe - net8.0 - enable - disable - true - 1701;1702;CS8321 - false - - USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS - - Debug;Release - - - - - - - - - - - - - Analyzer - false - - - - - - - PreserveNewest - - + + + + Exe + net9.0 + 13 + + enable + disable + true + 1701;1702;CS8321 + false + + USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS + + Debug;Release + + + + + + + + + + + + + + Analyzer + false + + + + + + + PreserveNewest + + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 2c0db53f..fbfdcc8c 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,15 +1,66 @@ #nullable enable +using CommunityToolkit.Mvvm.ComponentModel; using ConsoleAppFramework; using Microsoft.Extensions.DependencyInjection; -using System.Text.Json; -//using Microsoft.Extensions.Configuration; -//using Microsoft.Extensions.DependencyInjection; -//// using Microsoft.Extensions.Hosting; -//using Microsoft.Extensions.Logging; -//using Microsoft.Extensions.Options; -//using ZLogger; +using Microsoft.Extensions.Hosting; +[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] + +args = ["TestCommand", "Run"]; + +var builder = Host.CreateApplicationBuilder(args); +var services = builder.Services; + +services.AddSingleton(); +services.AddKeyedSingleton("Key"); + + +MyObj obj = new(); +while (obj.Data.Count < 1) +{ + obj.Data.Add(0); // <-- CAF008 error here +} + +var app = builder.ToConsoleAppBuilder(); +// var app = ConsoleApp.Create(); +//for (int i = 0; i < 10; i++) +//{ +// app.Add("foo", (int x) => { }); +//} + +app.Run(args); + +interface ITest +{ + int Value { get; set; } +} + +class Test : ITest +{ + public int Value { get; set; } = 1; +} + +class KeyedTest : ITest +{ + public int Value { get; set; } = 2; +} + +[RegisterCommands(nameof(TestCommand))] +class TestCommand([FromKeyedServices("Key")] ITest test) +{ + public void Run() + { + // This value should be 2 but 1 displayed + Console.WriteLine(test.Value); + } +} + +public partial class MyObj : ObservableObject +{ + [ObservableProperty] + private List data = []; +} //args = ["echo", "--msg", "zzzz"]; @@ -29,240 +80,282 @@ // services.Configure(configuration.GetSection("Position")); // }); -//app.Add(); -//app.Run(args); -// sc.BuildServiceProvider() +//// Uncomment following line to overwrite args. +// args = ["run", "--project", "foo.csproj", "--", "--foo", "100", "--bar", "bazbaz"]; -//IServiceProvider ser; -//ser.CreateScope() +// dotnet run --project foo.csproj -- --foo 100 --bar bazbaz -ConsoleApp.Run(args, () => { }); +//var app = ConsoleApp.Create(); -// inject options -//public class MyCommand(IOptions options) +//app.Add("run", ([FromKeyedServices("takoyaki")] List testList, string project, ConsoleAppContext context) => //{ -// public void Echo(string msg) -// { -// ConsoleApp.Log($"Binded Option: {options.Value.Title} {options.Value.Name}"); -// } -//} +// // run --project foo.csproj -- --foo 100 --bar bazbaz +// Console.WriteLine(string.Join(" ", context.Arguments)); -//public class PositionOptions -//{ -// public string Title { get; set; } = ""; -// public string Name { get; set; } = ""; -//} +// // --project foo.csproj +// Console.WriteLine(string.Join(" ", context.CommandArguments!)); + +// //IServiceProvider ServiceProvider = null!; +// // ((Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)ServiceProvider).GetKeyedService(Type, ""); + +// // FromKeyedServicesAttribute +// // IKeyedServiceProvider + +// // --foo 100 --bar bazbaz +// Console.WriteLine(string.Join(" ", context.EscapedArguments!)); +//}); + +//app.Add("foo"); + +//app.UseFilter(); + +//app.Run(args); -//internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next) + +//internal class NopFilter2([FromKeyedServices("mykey")] List xxxx, ConsoleAppFilter next) : ConsoleAppFilter(next) //{ // public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) // { -// // create Microsoft.Extensions.DependencyInjection scope -// await using var scope = serviceProvider.CreateAsyncScope(); - -// var originalServiceProvider = ConsoleApp.ServiceProvider; -// ConsoleApp.ServiceProvider = scope.ServiceProvider; // try // { -// await Next.InvokeAsync(context, cancellationToken); +// /* on before */ +// await Next.InvokeAsync(context, cancellationToken); // next +// /* on after */ +// } +// catch +// { +// /* on error */ +// throw; // } // finally // { -// ConsoleApp.ServiceProvider = originalServiceProvider; +// /* on finally */ // } // } //} -// JsonSerializer.Deserialize("foo"); -//// inject logger to filter -//internal class ReplaceLogFilter(ConsoleAppFilter next, ILogger logger) -// : ConsoleAppFilter(next) +////ConsoleApp.Run(args, (ConsoleAppContext ctx) => { }); + +//// inject options +////public class MyCommand(IOptions options) +////{ +//// public void Echo(string msg) +//// { +//// ConsoleApp.Log($"Binded Option: {options.Value.Title} {options.Value.Name}"); +//// } +////} + +////public class PositionOptions +////{ +//// public string Title { get; set; } = ""; +//// public string Name { get; set; } = ""; +////} + +////internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next) +////{ +//// public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) +//// { +//// // create Microsoft.Extensions.DependencyInjection scope +//// await using var scope = serviceProvider.CreateAsyncScope(); + +//// var originalServiceProvider = ConsoleApp.ServiceProvider; +//// ConsoleApp.ServiceProvider = scope.ServiceProvider; +//// try +//// { +//// await Next.InvokeAsync(context, cancellationToken); +//// } +//// finally +//// { +//// ConsoleApp.ServiceProvider = originalServiceProvider; +//// } +//// } +////} + +//// JsonSerializer.Deserialize("foo"); + +////// inject logger to filter +////internal class ReplaceLogFilter(ConsoleAppFilter next, ILogger logger) +//// : ConsoleAppFilter(next) +////{ +//// public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) +//// { +//// ConsoleApp.Log = msg => logger.LogInformation(msg); +//// ConsoleApp.LogError = msg => logger.LogError(msg); + +//// return Next.InvokeAsync(context, cancellationToken); +//// } +////} + +//class MyProvider : IServiceProvider, IAsyncDisposable //{ -// public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) +// public void Dispose() // { -// ConsoleApp.Log = msg => logger.LogInformation(msg); -// ConsoleApp.LogError = msg => logger.LogError(msg); +// Console.WriteLine("disposed"); +// } -// return Next.InvokeAsync(context, cancellationToken); +// public ValueTask DisposeAsync() +// { +// Console.WriteLine("dispose async"); +// return default; +// } + +// public object? GetService(Type serviceType) +// { +// return null; // } //} -class MyProvider : IServiceProvider, IAsyncDisposable -{ - public void Dispose() - { - Console.WriteLine("disposed"); - } +//public class MyService +//{ - public ValueTask DisposeAsync() - { - Console.WriteLine("dispose async"); - return default; - } +//} - public object? GetService(Type serviceType) - { - return null; - } -} -public class MyService -{ +//public class MyCommands +//{ +// public MyCommands([FromKeyedServices(10.9)] float kokonimo) +// { -} +// } +// public void Cmd1(int x, int y, ConsoleAppContext ctx) +// { +// } -public class MyCommands -{ - /// - /// - /// - /// foobarbaz! - [Command("Error1")] - public void Error1(string msg = @"\") - { - Console.WriteLine(msg); - } - [Command("Error2")] - public void Error2(string msg = "\\") - { - Console.WriteLine(msg); - } - [Command("Output")] - public void Output(string msg = @"\\") - { - Console.WriteLine(msg); // 「\」 - } -} +// public Task Cmd2([FromKeyedServices(typeof(int))] List l, int x, int y) +// { +// return Task.CompletedTask; +// } +//} -public class Tacommands -{ - public void HelloWorld(int hogeMoge) - { - } -} +//public class Tacommands +//{ +// public void HelloWorld(int hogeMoge) +// { +// } +//} -namespace ConsoleAppFramework -{ - internal static partial class ConsoleApp - { - static void Foo() - { - var options = JsonSerializerOptions ?? System.Text.Json.JsonSerializerOptions.Default; - } - - //public static ConsoleAppBuilder Create(IServiceProvider serviceProvider) - //{ - // ConsoleApp.ServiceProvider = serviceProvider; - // return ConsoleApp.Create(); - //} - - //public static ConsoleAppBuilder Create(Action configure) - //{ - // var services = new ServiceCollection(); - // configure(services); - // ConsoleApp.ServiceProvider = services.BuildServiceProvider(); - // return ConsoleApp.Create(); - //} - - - - //internal partial class ConsoleAppBuilder - //{ - // bool requireConfiguration; - // IConfiguration? configuration; - // Action? configureServices; - // Action? configureLogging; - - // /// Create configuration with SetBasePath(Directory.GetCurrentDirectory()) and AddJsonFile("appsettings.json"). - // public void ConfigureDefaultConfiguration(Action configure) - // { - // var config = new ConfigurationBuilder(); - // config.SetBasePath(System.IO.Directory.GetCurrentDirectory()); - // config.AddJsonFile("appsettings.json", optional: true); - // configure(config); - // configuration = config.Build(); - // } - - // public void ConfigureEmptyConfiguration(Action configure) - // { - // var config = new ConfigurationBuilder(); - // configure(config); - // configuration = config.Build(); - // } - - // public void ConfigureServices(Action configure) - // { - // this.configureServices = (_, services) => configure(services); - // } - - // public void ConfigureServices(Action configure) - // { - // this.requireConfiguration = true; - // this.configureServices = configure; - // } - - // public void ConfigureLogging(Action configure) - // { - // this.configureLogging = (_, builder) => configure(builder); - // } - - // public void ConfigureLogging(Action configure) - // { - // this.requireConfiguration = true; - // this.configureLogging = configure; - // } - - // public void BuildAndSetServiceProvider() - // { - // if (configureServices == null && configureLogging == null) return; - - // if (configureServices != null) - // { - // var services = new ServiceCollection(); - // configureServices?.Invoke(configuration!, services); - - // if (configureLogging != null) - // { - // var config = configuration; - // if (requireConfiguration && config == null) - // { - // config = new ConfigurationRoot(Array.Empty()); - // } - - // var configure = configureLogging; - // services.AddLogging(logging => - // { - // configure!(config!, logging); - // }); - // } - - // ConsoleApp.ServiceProvider = services.BuildServiceProvider(); - // } - // } - //} - } +//namespace ConsoleAppFramework +//{ +// internal static partial class ConsoleApp +// { +// static void Foo() +// { +// var options = JsonSerializerOptions ?? System.Text.Json.JsonSerializerOptions.Default; +// } +// //public static ConsoleAppBuilder Create(IServiceProvider serviceProvider) +// //{ +// // ConsoleApp.ServiceProvider = serviceProvider; +// // return ConsoleApp.Create(); +// //} + +// //public static ConsoleAppBuilder Create(Action configure) +// //{ +// // var services = new ServiceCollection(); +// // configure(services); +// // ConsoleApp.ServiceProvider = services.BuildServiceProvider(); +// // return ConsoleApp.Create(); +// //} + + + +// //internal partial class ConsoleAppBuilder +// //{ +// // bool requireConfiguration; +// // IConfiguration? configuration; +// // Action? configureServices; +// // Action? configureLogging; + +// // /// Create configuration with SetBasePath(Directory.GetCurrentDirectory()) and AddJsonFile("appsettings.json"). +// // public void ConfigureDefaultConfiguration(Action configure) +// // { +// // var config = new ConfigurationBuilder(); +// // config.SetBasePath(System.IO.Directory.GetCurrentDirectory()); +// // config.AddJsonFile("appsettings.json", optional: true); +// // configure(config); +// // configuration = config.Build(); +// // } + +// // public void ConfigureEmptyConfiguration(Action configure) +// // { +// // var config = new ConfigurationBuilder(); +// // configure(config); +// // configuration = config.Build(); +// // } + +// // public void ConfigureServices(Action configure) +// // { +// // this.configureServices = (_, services) => configure(services); +// // } + +// // public void ConfigureServices(Action configure) +// // { +// // this.requireConfiguration = true; +// // this.configureServices = configure; +// // } + +// // public void ConfigureLogging(Action configure) +// // { +// // this.configureLogging = (_, builder) => configure(builder); +// // } + +// // public void ConfigureLogging(Action configure) +// // { +// // this.requireConfiguration = true; +// // this.configureLogging = configure; +// // } + +// // public void BuildAndSetServiceProvider() +// // { +// // if (configureServices == null && configureLogging == null) return; + +// // if (configureServices != null) +// // { +// // var services = new ServiceCollection(); +// // configureServices?.Invoke(configuration!, services); + +// // if (configureLogging != null) +// // { +// // var config = configuration; +// // if (requireConfiguration && config == null) +// // { +// // config = new ConfigurationRoot(Array.Empty()); +// // } + +// // var configure = configureLogging; +// // services.AddLogging(logging => +// // { +// // configure!(config!, logging); +// // }); +// // } + +// // ConsoleApp.ServiceProvider = services.BuildServiceProvider(); +// // } +// // } +// //} +// } -} +//} -namespace HogeHoge -{ +//namespace HogeHoge +//{ - public class BatchAttribute : Attribute - { - } +// public class BatchAttribute : Attribute +// { +// } - public class Batch2Attribute : BatchAttribute - { - } + +// public class Batch2Attribute : BatchAttribute +// { +// } -} \ No newline at end of file +//} diff --git a/sandbox/GeneratorSandbox/Properties/launchSettings.json b/sandbox/GeneratorSandbox/Properties/launchSettings.json new file mode 100644 index 00000000..91cb9250 --- /dev/null +++ b/sandbox/GeneratorSandbox/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "profiles": { + "Default": { + "commandName": "Project", + "commandLineArgs": "run --project foo.csproj -- --foo 100 --bar bazbaz" + }, + "ShowVersion": { + "commandName": "Project", + "commandLineArgs": "--version" + }, + "ShowRootCommandHelp": { + "commandName": "Project", + "commandLineArgs": "--help" + }, + "ShowRunCommandHelp": { + "commandName": "Project", + "commandLineArgs": "run --help" + } + } +} diff --git a/sandbox/NativeAot/NativeAot.csproj b/sandbox/NativeAot/NativeAot.csproj index b44e82cb..6c599b38 100644 --- a/sandbox/NativeAot/NativeAot.csproj +++ b/sandbox/NativeAot/NativeAot.csproj @@ -1,22 +1,26 @@  - - Exe - net9.0 - enable - enable - false + + Exe + net9.0 + enable + enable + false - true - true - Debug;Release - + true + true + true + true + true + + Debug;Release + - - - Analyzer - false - - + + + Analyzer + false + + diff --git a/sandbox/NativeAot/Program.cs b/sandbox/NativeAot/Program.cs index 400798ea..4251bd79 100644 --- a/sandbox/NativeAot/Program.cs +++ b/sandbox/NativeAot/Program.cs @@ -8,6 +8,7 @@ ConsoleApp.Run(args, (int x, Kabayaki y) => Console.WriteLine(x + y.MyProperty)); app.Run(args); + public class Kabayaki { public int MyProperty { get; set; } diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs b/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs index be56be9f..69d28e32 100644 --- a/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs +++ b/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs @@ -1,11 +1,52 @@ -namespace ConsoleAppFramework; +using System.ComponentModel; + +namespace ConsoleAppFramework; public interface IArgumentParser { static abstract bool TryParse(ReadOnlySpan s, out T result); } -public record class ConsoleAppContext(string CommandName, string[] Arguments, object? State); +public record ConsoleAppContext +{ + public string CommandName { get; init; } + public string[] Arguments { get; init; } + public object? State { get; init; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int CommandDepth { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public int EscapeIndex { get; } + + public ReadOnlySpan CommandArguments + { + get => (EscapeIndex == -1) + ? Arguments.AsSpan(CommandDepth) + : Arguments.AsSpan(CommandDepth, EscapeIndex - CommandDepth); + } + + public ReadOnlySpan EscapedArguments + { + get => (EscapeIndex == -1) + ? Array.Empty() + : Arguments.AsSpan(EscapeIndex + 1); + } + + public ConsoleAppContext(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex) + { + this.CommandName = commandName; + this.Arguments = arguments; + this.State = state; + this.CommandDepth = commandDepth; + this.EscapeIndex = escapeIndex; + } + + public override string ToString() + { + return string.Join(" ", Arguments); + } +} public abstract class ConsoleAppFilter(ConsoleAppFilter next) { @@ -22,4 +63,4 @@ public sealed class ConsoleAppFilterAttribute : Attribute public sealed class ArgumentParseFailedException(string message) : Exception(message) { -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj index 80cabfef..f767a6c2 100644 --- a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj +++ b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj @@ -1,19 +1,19 @@  - - net8.0 - enable - enable + + net8.0 + enable + enable - - ConsoleAppFramework.Abstractions - ConsoleAppFramework external abstractions library. - Debug;Release - + + ConsoleAppFramework.Abstractions + ConsoleAppFramework external abstractions library. + Debug;Release + - - - - + + + + diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props index 1019de1d..658c0320 100644 --- a/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props +++ b/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props @@ -1,5 +1,5 @@ - - - USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS - - \ No newline at end of file + + + USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS + + diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 1f74ddf6..7d67f23a 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -18,6 +18,7 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int + public required bool IsHidden { get; init; } // Hide help from command list public bool IsRootCommand => Name == ""; public required string Name { get; init; } @@ -153,15 +154,18 @@ public record class CommandParameter public required IgnoreEquality WellKnownTypes { get; init; } public required bool IsNullableReference { get; init; } public required bool IsParams { get; init; } + public required bool IsHidden { get; init; } // Hide command parameter help public required string Name { get; init; } public required string OriginalParameterName { get; init; } public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } public required EquatableTypeSymbol? CustomParserType { get; init; } public required bool IsFromServices { get; init; } + public required bool IsFromKeyedServices { get; init; } + public required object? KeyedServiceKey { get; init; } public required bool IsConsoleAppContext { get; init; } public required bool IsCancellationToken { get; init; } - public bool IsParsable => !(IsFromServices || IsCancellationToken || IsConsoleAppContext); + public bool IsParsable => !(IsFromServices || IsFromKeyedServices || IsCancellationToken || IsConsoleAppContext); public bool IsFlag => Type.SpecialType == SpecialType.System_Boolean; public required bool HasValidation { get; init; } public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] @@ -173,7 +177,7 @@ public record class CommandParameter // increment = false when passed from [Argument] public string BuildParseMethod(int argCount, string argumentName, bool increment) { - var incrementIndex = increment ? "!TryIncrementIndex(ref i, args.Length) || " : ""; + var incrementIndex = increment ? "!TryIncrementIndex(ref i, commandArgs.Length) || " : ""; return Core(Type.TypeSymbol, false); string Core(ITypeSymbol type, bool nullable) @@ -193,7 +197,7 @@ string Core(ITypeSymbol type, bool nullable) if (CustomParserType != null) { - return $"if ({incrementIndex}!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"if ({incrementIndex}!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(commandArgs[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } switch (type.SpecialType) @@ -202,11 +206,11 @@ string Core(ITypeSymbol type, bool nullable) // no parse if (increment) { - return $"if (!TryIncrementIndex(ref i, args.Length)) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }} else {{ arg{argCount} = args[i]; }}"; + return $"if (!TryIncrementIndex(ref i, commandArgs.Length)) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }} else {{ arg{argCount} = commandArgs[i]; }}"; } else { - return $"arg{argCount} = args[i];"; + return $"arg{argCount} = commandArgs[i];"; } case SpecialType.System_Boolean: @@ -230,13 +234,13 @@ string Core(ITypeSymbol type, bool nullable) // Enum if (type.TypeKind == TypeKind.Enum) { - return $"if ({incrementIndex}!Enum.TryParse<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[i], true, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"if ({incrementIndex}!Enum.TryParse<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(commandArgs[i], true, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } // ParamsArray if (IsParams) { - return $"{(increment ? "i++; " : "")}if (!TryParseParamsArray(args, ref arg{argCount}, ref i)) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"{(increment ? "i++; " : "")}if (!TryParseParamsArray(commandArgs, ref arg{argCount}, ref i)) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } // Array @@ -248,7 +252,7 @@ string Core(ITypeSymbol type, bool nullable) { if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) { - return $"if ({incrementIndex}!TrySplitParse(args[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"if ({incrementIndex}!TrySplitParse(commandArgs[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } } break; @@ -272,15 +276,15 @@ string Core(ITypeSymbol type, bool nullable) if (tryParseKnownPrimitive) { - return $"if ({incrementIndex}!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"if ({incrementIndex}!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(commandArgs[i], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } else if (tryParseIParsable) { - return $"if ({incrementIndex}!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[i], null, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + return $"if ({incrementIndex}!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(commandArgs[i], null, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}{elseExpr}"; } else { - return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{type.ToFullyQualifiedFormatDisplayString()}>(args[{(increment ? "++i" : "i")}], JsonSerializerOptions); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; + return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{type.ToFullyQualifiedFormatDisplayString()}>(commandArgs[{(increment ? "++i" : "i")}], JsonSerializerOptions); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", commandArgs[i]); }}"; } } } @@ -330,6 +334,25 @@ public string ToTypeShortString() return IsNullableReference ? $"{t}?" : t; } + public string GetFormattedKeyedServiceKey() + { + return GetFormattedKeyedServiceKey(KeyedServiceKey); + } + + public static string GetFormattedKeyedServiceKey(object? keyedServiceKey) + { + if (keyedServiceKey == null) return "null"; + + if (keyedServiceKey is string) return $"\"{keyedServiceKey}\""; + + if (keyedServiceKey is ITypeSymbol type) + { + return $"typeof({type.ToFullyQualifiedFormatDisplayString()})"; + } + + return $"({keyedServiceKey.GetType().FullName}){keyedServiceKey.ToString()}"; + } + public override string ToString() { var sb = new StringBuilder(); @@ -354,7 +377,7 @@ public record class CommandMethodInfo { public required string TypeFullName { get; init; } public required string MethodName { get; init; } - public required EquatableArray ConstructorParameterTypes { get; init; } + public required EquatableArray ConstructorParameterTypes { get; init; } public required bool IsIDisposable { get; init; } public required bool IsIAsyncDisposable { get; init; } @@ -363,7 +386,14 @@ public string BuildNew() var p = ConstructorParameterTypes.Select(parameter => { var type = parameter.ToFullyQualifiedFormatDisplayString(); - return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + if (!parameter.IsKeyedService) + { + return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + } + else + { + return $"({type})((Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)ServiceProvider).GetKeyedService(typeof({type}), {parameter.FormattedKeyedServiceKey})!"; + } }); return $"new {TypeFullName}({string.Join(", ", p)})"; @@ -373,7 +403,7 @@ public string BuildNew() public record class FilterInfo { public required string TypeFullName { get; init; } - public required EquatableArray ConstructorParameterTypes { get; init; } + public required EquatableArray ConstructorParameterTypes { get; init; } FilterInfo() { @@ -395,7 +425,12 @@ public record class FilterInfo var filter = new FilterInfo { TypeFullName = type.ToFullyQualifiedFormatDisplayString(), - ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => new EquatableTypeSymbol(x.Type)).ToArray() + ConstructorParameterTypes = publicConstructors[0].Parameters + .Select(x => + { + return new EquatableTypeSymbolWithKeyedServiceKey(x); + }) + .ToArray() }; return filter; @@ -412,10 +447,17 @@ public string BuildNew(string nextFilterName) } else { - return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + if (!parameter.IsKeyedService) + { + return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + } + else + { + return $"({type})((Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)ServiceProvider).GetKeyedService(typeof({type}), {parameter.FormattedKeyedServiceKey})!"; + } } }); return $"new {TypeFullName}({string.Join(", ", p)})"; } -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/CommandHelpBuilder.cs b/src/ConsoleAppFramework/CommandHelpBuilder.cs index 0e7a73e7..b6b44386 100644 --- a/src/ConsoleAppFramework/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework/CommandHelpBuilder.cs @@ -64,7 +64,7 @@ static string BuildHelpMessageCore(Command command, bool showCommandName, bool s if (definition.Options.Any()) { var hasArgument = definition.Options.Any(x => x.Index.HasValue); - var hasOptions = definition.Options.Any(x => !x.Index.HasValue); + var hasNoHiddenOptions = definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden); if (hasArgument) { @@ -72,7 +72,7 @@ static string BuildHelpMessageCore(Command command, bool showCommandName, bool s sb.AppendLine(BuildArgumentsMessage(definition)); } - if (hasOptions) + if (hasNoHiddenOptions) { sb.AppendLine(); sb.AppendLine(BuildOptionsMessage(definition)); @@ -102,7 +102,7 @@ static string BuildUsageMessage(CommandHelpDefinition definition, bool showComma sb.Append(" [arguments...]"); } - if (definition.Options.Any(x => !x.Index.HasValue)) + if (definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden)) { sb.Append(" [options...]"); } @@ -160,6 +160,7 @@ static string BuildOptionsMessage(CommandHelpDefinition definition) { var optionsFormatted = definition.Options .Where(x => !x.Index.HasValue) + .Where(x => !x.IsHidden) .Select(x => (Options: string.Join("|", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) .ToArray(); @@ -215,6 +216,7 @@ static string BuildOptionsMessage(CommandHelpDefinition definition) static string BuildMethodListMessage(IEnumerable commands, out int maxWidth) { var formatted = commands + .Where(x => !x.IsHidden) .Select(x => { return (Command: x.Name, x.Description); @@ -279,6 +281,7 @@ static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) var description = item.Description; var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; var isParams = item.IsParams; + var isHidden = item.IsHidden; var defaultValue = default(string); if (item.HasDefaultValue) @@ -300,7 +303,7 @@ static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) } var paramTypeName = item.ToTypeShortString(); - parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams)); + parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams, isHidden)); } var commandName = descriptor.Name; @@ -336,9 +339,10 @@ class CommandOptionHelpDefinition public bool IsRequired => DefaultValue == null && !IsParams; public bool IsFlag { get; } public bool IsParams { get; } + public bool IsHidden { get; } public string FormattedValueTypeName => "<" + ValueTypeName + ">"; - public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams) + public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden) { Options = options; Description = description; @@ -347,6 +351,7 @@ public CommandOptionHelpDefinition(string[] options, string description, string Index = index; IsFlag = isFlag; IsParams = isParams; + IsHidden = isHidden; } } -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs index f69f1548..576cb6b1 100644 --- a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs +++ b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs @@ -8,7 +8,7 @@ public static class ConsoleAppBaseCode #pragma warning disable namespace ConsoleAppFramework; - + using System; using System.Text; using System.Reflection; @@ -45,7 +45,42 @@ internal interface IArgumentParser static abstract bool TryParse(ReadOnlySpan s, out T result); } -internal record class ConsoleAppContext(string CommandName, string[] Arguments, object? State); +internal record ConsoleAppContext +{ + public string CommandName { get; init; } + public string[] Arguments { get; init; } + public object? State { get; init; } + internal int CommandDepth { get; } + internal int EscapeIndex { get; } + + public ReadOnlySpan CommandArguments + { + get => (EscapeIndex == -1) + ? Arguments.AsSpan(CommandDepth) + : Arguments.AsSpan(CommandDepth, EscapeIndex - CommandDepth); + } + + public ReadOnlySpan EscapedArguments + { + get => (EscapeIndex == -1) + ? Array.Empty() + : Arguments.AsSpan(EscapeIndex + 1); + } + + public ConsoleAppContext(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex) + { + this.CommandName = commandName; + this.Arguments = arguments; + this.State = state; + this.CommandDepth = commandDepth; + this.EscapeIndex = escapeIndex; + } + + public override string ToString() + { + return string.Join(" ", Arguments); + } +} internal abstract class ConsoleAppFilter(ConsoleAppFilter next) { @@ -87,6 +122,11 @@ public CommandAttribute(string command) } } +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class HiddenAttribute : Attribute +{ +} + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] internal sealed class RegisterCommandsAttribute : Attribute { @@ -109,6 +149,8 @@ public class ConsoleAppFrameworkGeneratorOptionsAttribute : Attribute public bool DisableNamingConversion { get; set; } } +[UnconditionalSuppressMessage("Trimming", "IL2026")] +[UnconditionalSuppressMessage("AOT", "IL3050")] internal static partial class ConsoleApp { public static IServiceProvider? ServiceProvider { get; set; } @@ -272,7 +314,7 @@ static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParamete if (args.Length == 0) { if (requiredParameterCount == 0) return false; - + ShowHelp(helpId); return true; } @@ -329,12 +371,12 @@ static void ShowVersion() static partial void ShowHelp(int helpId); - static async Task RunWithFilterAsync(string commandName, string[] args, ConsoleAppFilter invoker) + static async Task RunWithFilterAsync(string commandName, string[] args, int commandDepth, int escapeIndex, ConsoleAppFilter invoker) { using var posixSignalHandler = PosixSignalHandler.Register(Timeout); try { - await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); + await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null, commandDepth, escapeIndex), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); } catch (Exception ex) { @@ -496,7 +538,7 @@ static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParamete if (args.Length == 0) { if (requiredParameterCount == 0) return false; - + ShowHelp(helpId); return true; } diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index 8f33a205..363e1b74 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -1,50 +1,50 @@  - - netstandard2.0 - 12 - enable - enable - ConsoleAppFramework - true - cs - - - false - true - false - true - true - - - ConsoleAppFramework - Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. - Debug;Release - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - + + netstandard2.0 + 12 + enable + enable + ConsoleAppFramework + true + cs + + + false + true + false + true + true + + + ConsoleAppFramework + Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. + Debug;Release + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index dedef6e1..5cc1bb8c 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -15,7 +15,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Emit ConsoleApp.g.cs context.RegisterPostInitializationOutput(EmitConsoleAppTemplateSource); - // Emti ConfigureConfiguration/Logging/Services and Host.AsConsoleApp + // Emit ConfigureConfiguration/Logging/Services and Host.AsConsoleApp var hasReferences = context.MetadataReferencesProvider .Collect() .Select((xs, _) => @@ -31,7 +31,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var name = x.Display; if (name == null) continue; - if (!hasDependencyInjection && name.EndsWith("Microsoft.Extensions.DependencyInjection.dll")) // BuildServiceProvider + if (!hasDependencyInjection && name.EndsWith("Microsoft.Extensions.DependencyInjection.dll")) // BuildServiceProvider, IKeyedServiceProvider { hasDependencyInjection = true; continue; @@ -118,7 +118,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var isRunAsync = (node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"; var command = parser.ParseAndValidateForRun(); - return new CommanContext(command, isRunAsync, reporter, node); + return new CommandContext(command, isRunAsync, reporter, node); }) .WithTrackingName("ConsoleApp.Run.0_CreateSyntaxProvider"); // annotate for IncrementalGeneratorTest @@ -151,11 +151,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.SemanticModel, ct)) .WithTrackingName("ConsoleApp.Builder.0_CreateSyntaxProvider") - .Where(x => + .Select((x, ct) => { var model = x.Model.GetTypeInfo((x.Node.Expression as MemberAccessExpressionSyntax)!.Expression, x.CancellationToken); - return model.Type?.Name is "ConsoleAppBuilder" or "IHostBuilder" || model.Type?.Kind == SymbolKind.ErrorType; // allow ErrorType(ConsoleAppBuilder from Configure***(Source Generator generated method) is unknown in Source Generator) + return (x, model.Type?.Name, model.Type?.Kind); }) + .Where(x => x.Name is "ConsoleAppBuilder" or "IHostBuilder" || x.Kind == SymbolKind.ErrorType) // allow ErrorType(ConsoleAppBuilder from Configure***(Source Generator generated method) is unknown in Source Generator) .WithTrackingName("ConsoleApp.Builder.1_Where") .Collect() .Combine(generatorOptions) @@ -182,10 +183,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) { - context.AddSource("ConsoleApp.cs", ConsoleAppBaseCode.InitializationCode); + context.AddSource("ConsoleApp.g.cs", ConsoleAppBaseCode.InitializationCode.ReplaceLineEndings()); } - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommanContext commandContext) + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommandContext commandContext) { if (commandContext.DiagnosticReporter.HasDiagnostics) { @@ -209,7 +210,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, C var withId = new Emitter.CommandWithId(null, command, -1); emitter.EmitRun(sb, withId, command.IsAsync); } - sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString().ReplaceLineEndings()); var help = new SourceBuilder(0); help.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); @@ -218,7 +219,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, C var emitter = new Emitter(); emitter.EmitHelp(help, command); } - sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString().ReplaceLineEndings()); } static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, CollectBuilderContext collectBuilderContext) @@ -267,7 +268,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var emitter = new Emitter(); emitter.EmitBuilder(sb, commandIds, hasRun, hasRunAsync); } - sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString().ReplaceLineEndings()); // Build Help @@ -279,7 +280,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var emitter = new Emitter(); emitter.EmitHelp(help, commandIds!); } - sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString().ReplaceLineEndings()); } static void EmitConsoleAppConfigure(SourceProductionContext sourceProductionContext, DllReference dllReference) @@ -311,7 +312,7 @@ static void EmitConsoleAppConfigure(SourceProductionContext sourceProductionCont sb2.AppendLine("using Microsoft.Extensions.Hosting;"); var emitter = new Emitter(); emitter.EmitAsConsoleAppBuilder(sb2, dllReference); - sourceProductionContext.AddSource("ConsoleAppHostBuilderExtensions.g.cs", sb2.ToString()); + sourceProductionContext.AddSource("ConsoleAppHostBuilderExtensions.g.cs", sb2.ToString().ReplaceLineEndings()); } using (sb.BeginBlock("internal static partial class ConsoleApp")) @@ -321,17 +322,17 @@ static void EmitConsoleAppConfigure(SourceProductionContext sourceProductionCont emitter.EmitConfigure(sb, dllReference); } - sourceProductionContext.AddSource("ConsoleApp.Builder.Configure.g.cs", sb.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Builder.Configure.g.cs", sb.ToString().ReplaceLineEndings()); } - class CommanContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable + class CommandContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable { public Command? Command => command; public DiagnosticReporter DiagnosticReporter => diagnosticReporter; public InvocationExpressionSyntax Node => node; public bool IsAsync => isAsync; - public bool Equals(CommanContext other) + public bool Equals(CommandContext other) { // has diagnostics, always go to modified(don't cache) if (diagnosticReporter.HasDiagnostics || other.DiagnosticReporter.HasDiagnostics) return false; @@ -353,7 +354,7 @@ class CollectBuilderContext : IEquatable FilterInfo[]? globalFilters { get; } ConsoleAppFrameworkGeneratorOptions generatorOptions { get; } - public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOptions, ImmutableArray contexts, CancellationToken cancellationToken) + public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOptions, ImmutableArray<(BuilderContext, string?, SymbolKind?)> contexts, CancellationToken cancellationToken) { this.DiagnosticReporter = new DiagnosticReporter(); this.CancellationToken = cancellationToken; @@ -362,19 +363,24 @@ public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOption // validation, invoke in loop is not allowed. foreach (var item in contexts) { - if (item.Name is "Run" or "RunAsync") continue; - foreach (var n in item.Node.Ancestors()) + var (ctx, name, kind) = item; + if (kind == SymbolKind.ErrorType) continue; // ErrorType can't distinguished from ConsoleAppFramework or others so ignore all. + + if (ctx.Name is "Run" or "RunAsync") continue; + foreach (var n in ctx.Node.Ancestors()) { if (n.Kind() is SyntaxKind.WhileStatement or SyntaxKind.DoStatement or SyntaxKind.ForStatement or SyntaxKind.ForEachStatement) { - DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.AddInLoopIsNotAllowed, item.Node.GetLocation()); + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.AddInLoopIsNotAllowed, ctx.Node.GetLocation()); return; } } } - var methodGroup = contexts.ToLookup(x => + var methodGroup = contexts.ToLookup(ctx => { + var x = ctx.Item1; + if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) { return "Add"; @@ -384,6 +390,7 @@ public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOption }); globalFilters = methodGroup["UseFilter"] + .Select(x => x.Item1) .OrderBy(x => x.Node.GetLocation().SourceSpan) // sort by line number .Select(x => { @@ -396,7 +403,7 @@ public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOption if (filter == null) { - DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, genericType.GetLocation()); + DiagnosticReporter.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConstructor, genericType.GetLocation()); return null!; } @@ -413,6 +420,7 @@ public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOption var names = new HashSet(); var commands1 = methodGroup["Add"] + .Select(x => x.Item1) .Select(x => { var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); @@ -432,6 +440,7 @@ public CollectBuilderContext(ConsoleAppFrameworkGeneratorOptions generatorOption .ToArray(); // evaluate first. var commands2 = methodGroup["Add"] + .Select(x => x.Item1) .SelectMany(x => { var wellKnownTypes = new WellKnownTypes(x.Model.Compilation); diff --git a/src/ConsoleAppFramework/DiagnosticDescriptors.cs b/src/ConsoleAppFramework/DiagnosticDescriptors.cs index 0c54422f..6eb8b870 100644 --- a/src/ConsoleAppFramework/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework/DiagnosticDescriptors.cs @@ -95,11 +95,11 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo 9, "ConsoleApp.Run does not allow the use of filters, but the function has a filter attribute."); - public static DiagnosticDescriptor FilterMultipleConsturtor { get; } = Create( + public static DiagnosticDescriptor FilterMultipleConstructor { get; } = Create( 10, "ConsoleAppFilter class does not allow multiple constructors."); - public static DiagnosticDescriptor ClassMultipleConsturtor { get; } = Create( + public static DiagnosticDescriptor ClassMultipleConstructor { get; } = Create( 11, "ConsoleAppBuilder.Add class does not allow multiple constructors."); diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index fc4e38ea..83b48e15 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -25,7 +25,6 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } var returnType = isRunAsync ? "async Task" : "void"; var accessibility = !emitForBuilder ? "public" : "private"; - var argsType = !emitForBuilder ? "string[]" : (isRunAsync ? "string[]" : "ReadOnlySpan"); // NOTE: C# 13 will allow Span in async methods so can change to ReadOnlyMemory(and store .Span in local var) methodName = methodName ?? (isRunAsync ? "RunAsync" : "Run"); var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; @@ -42,8 +41,8 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine(); } + var commandDepthEscapeIndex = emitForBuilder ? ", int commandDepth, int escapeIndex" : ""; var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; - var rawArgs = !emitForBuilder ? "" : "string[] rawArgs, "; if (!emitForBuilder) { @@ -59,9 +58,26 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } // method signature - using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({rawArgs}{argsType} args{commandMethodType}{filterCancellationToken})")) + using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}(string[] args{commandDepthEscapeIndex}{commandMethodType}{filterCancellationToken})")) { - sb.AppendLine($"if (TryShowHelpOrVersion(args, {requiredParsableParameterCount}, {commandWithId.Id})) return;"); + if (emitForBuilder) + { + sb.AppendLine("var commandArgs = (escapeIndex == -1) ? args.AsSpan(commandDepth) : args.AsSpan(commandDepth, escapeIndex - commandDepth);"); + } + else + { + if (hasConsoleAppContext) + { + sb.AppendLine("var escapeIndex = args.AsSpan().IndexOf(\"--\");"); + sb.AppendLine("var commandArgs = (escapeIndex == -1) ? args.AsSpan() : args.AsSpan(0, escapeIndex);"); + } + else + { + sb.AppendLine("var commandArgs = args.AsSpan();"); + } + } + + sb.AppendLine($"if (TryShowHelpOrVersion(commandArgs, {requiredParsableParameterCount}, {commandWithId.Id})) return;"); sb.AppendLine(); // prepare argument variables @@ -71,8 +87,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } if (hasConsoleAppContext) { - var rawArgsName = !emitForBuilder ? "args" : "rawArgs"; - sb.AppendLine($"var context = new ConsoleAppContext(\"{command.Name}\", {rawArgsName}, null);"); + sb.AppendLine($"var context = new ConsoleAppContext(\"{command.Name}\", args, null, {(emitForBuilder ? "commandDepth" : "0")}, escapeIndex);"); } for (var i = 0; i < command.Parameters.Length; i++) { @@ -108,12 +123,18 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); sb.AppendLine($"var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); } + else if (parameter.IsFromKeyedServices) + { + var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); + var line = $"var arg{i} = ({type})((Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)ServiceProvider).GetKeyedService(typeof({type}), {parameter.GetFormattedKeyedServiceKey()})!;"; + sb.AppendLine(line); + } } sb.AppendLineIfExists(command.Parameters.AsSpan()); using (command.HasFilter ? sb.Nop : sb.BeginBlock("try")) { - using (sb.BeginBlock("for (int i = 0; i < args.Length; i++)")) + using (sb.BeginBlock("for (int i = 0; i < commandArgs.Length; i++)")) { // parse indexed argument([Argument] parameter) if (hasArgument) @@ -137,7 +158,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine(); } - sb.AppendLine("var name = args[i];"); + sb.AppendLine("var name = commandArgs[i];"); sb.AppendLine(); using (sb.BeginBlock("switch (name)")) @@ -470,16 +491,16 @@ public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitS void EmitRunBody(ILookup groupedCommands, int depth, bool isRunAsync) { var leafCommand = groupedCommands[""].FirstOrDefault(); - IDisposable? ifBlcok = null; + IDisposable? ifBlock = null; if (!(groupedCommands.Count == 1 && leafCommand != null)) { - ifBlcok = sb.BeginBlock($"if (args.Length == {depth})"); + ifBlock = sb.BeginBlock($"if (args.Length == {depth})"); } EmitLeafCommand(leafCommand); - if (ifBlcok != null) + if (ifBlock != null) { sb.AppendLine("return;"); - ifBlcok.Dispose(); + ifBlock.Dispose(); } else { @@ -538,16 +559,16 @@ void EmitLeafCommand(CommandWithId? command) { if (!isRunAsync) { - sb.AppendLine($"RunCommand{command.Id}(args, args.AsSpan({depth}){commandArgs});"); + sb.AppendLine($"RunCommand{command.Id}(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs});"); } else { - sb.AppendLine($"result = RunCommand{command.Id}Async(args, args[{depth}..]{commandArgs});"); + sb.AppendLine($"result = RunCommand{command.Id}Async(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs});"); } } else { - var invokeCode = $"RunWithFilterAsync(\"{command.Command.Name}\", args, new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; + var invokeCode = $"RunWithFilterAsync(\"{command.Command.Name}\", args, {depth}, args.AsSpan().IndexOf(\"--\"), new Command{command.Id}Invoker({commandArgs.TrimStart(',', ' ')}).BuildFilter())"; if (!isRunAsync) { sb.AppendLine($"{invokeCode}.GetAwaiter().GetResult();"); @@ -565,9 +586,9 @@ void EmitFilterInvoker(CommandWithId command) { var commandType = command.Command.BuildDelegateSignature(command.BuildCustomDelegateTypeName(), out _); var needsCommand = commandType != null; - if (needsCommand) commandType = $", {commandType} command"; + if (needsCommand) commandType = $"{commandType} command"; - using (sb.BeginBlock($"sealed class Command{command.Id}Invoker(string[] args{commandType}) : ConsoleAppFilter(null!)")) + using (sb.BeginBlock($"sealed class Command{command.Id}Invoker({commandType}) : ConsoleAppFilter(null!)")) { using (sb.BeginBlock($"public ConsoleAppFilter BuildFilter()")) { @@ -584,7 +605,7 @@ void EmitFilterInvoker(CommandWithId command) using (sb.BeginBlock($"public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)")) { var cmdArgs = needsCommand ? ", command" : ""; - sb.AppendLine($"return RunCommand{command.Id}Async(context.Arguments, args{cmdArgs}, context, cancellationToken);"); + sb.AppendLine($"return RunCommand{command.Id}Async(context.Arguments, context.CommandDepth, context.EscapeIndex{cmdArgs}, context, cancellationToken);"); } } } @@ -817,13 +838,23 @@ public void EmitAsConsoleAppBuilder(SourceBuilder sb, DllReference dllReference) internal static class ConsoleAppHostBuilderExtensions { class CompositeDisposableServiceProvider(IDisposable host, IServiceProvider serviceServiceProvider, IDisposable scope, IServiceProvider serviceProvider) - : IServiceProvider, IDisposable + : IServiceProvider, IKeyedServiceProvider, IDisposable { public object? GetService(Type serviceType) { return serviceProvider.GetService(serviceType); } + public object? GetKeyedService(Type serviceType, object? serviceKey) + { + return ((IKeyedServiceProvider)serviceProvider).GetKeyedService(serviceType, serviceKey); + } + + public object GetRequiredKeyedService(Type serviceType, object? serviceKey) + { + return ((IKeyedServiceProvider)serviceProvider).GetRequiredKeyedService(serviceType, serviceKey); + } + public void Dispose() { if (serviceProvider is IDisposable d) diff --git a/src/ConsoleAppFramework/EquatableTypeSymbol.cs b/src/ConsoleAppFramework/EquatableTypeSymbol.cs index a9f67745..308c381d 100644 --- a/src/ConsoleAppFramework/EquatableTypeSymbol.cs +++ b/src/ConsoleAppFramework/EquatableTypeSymbol.cs @@ -27,7 +27,38 @@ public bool Equals(EquatableTypeSymbol other) } } +// for filter +public class EquatableTypeSymbolWithKeyedServiceKey + : EquatableTypeSymbol, IEquatable +{ + public bool IsKeyedService { get; } + public string? FormattedKeyedServiceKey { get; } + + public EquatableTypeSymbolWithKeyedServiceKey(IParameterSymbol symbol) + : base(symbol.Type) + { + var keyedServciesAttr = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "FromKeyedServicesAttribute"); + if (keyedServciesAttr != null) + { + this.IsKeyedService = true; + this.FormattedKeyedServiceKey = CommandParameter.GetFormattedKeyedServiceKey(keyedServciesAttr.ConstructorArguments[0].Value); + } + } + + public bool Equals(EquatableTypeSymbolWithKeyedServiceKey other) + { + if (base.Equals(other)) + { + if (IsKeyedService != other.IsKeyedService) return false; + if (FormattedKeyedServiceKey != other.FormattedKeyedServiceKey) return false; + return true; + } + + return false; + } +} + static class EquatableTypeSymbolExtensions { public static EquatableTypeSymbol ToEquatable(this ITypeSymbol typeSymbol) => new(typeSymbol); -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index be1320ef..3e024ea8 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -25,7 +25,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag public Command? ParseAndValidateForBuilderDelegateRegistration() // for ConsoleAppBuilder.Add { - // Add(string commandName, Delgate command) + // Add(string commandName, Delegate command) var args = (node as InvocationExpressionSyntax)!.ArgumentList.Arguments; if (args.Count == 2) // 0 = string command, 1 = lambda { @@ -112,7 +112,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag if (publicConstructors.Length != 1) { - context.ReportDiagnostic(DiagnosticDescriptors.ClassMultipleConsturtor, node.GetLocation()); + context.ReportDiagnostic(DiagnosticDescriptors.ClassMultipleConstructor, node.GetLocation()); return []; } @@ -128,7 +128,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag if (filter == null) { - context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConstructor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); return null!; } @@ -145,8 +145,8 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag TypeFullName = type.ToFullyQualifiedFormatDisplayString(), IsIDisposable = hasIDisposable, IsIAsyncDisposable = hasIAsyncDisposable, - ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => new EquatableTypeSymbol(x.Type)).ToArray(), - MethodName = "", // without methodname + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => new EquatableTypeSymbolWithKeyedServiceKey(x)).ToArray(), + MethodName = "", // without method name }; return publicMethods @@ -285,6 +285,10 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag var hasParams = x.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword)); + var isHidden = x.AttributeLists + .SelectMany(x => x.Attributes) + .Any(x => model.GetTypeInfo(x).Type?.Name == "HiddenAttribute"); + var customParserType = x.AttributeLists.SelectMany(x => x.Attributes) .Select(x => { @@ -321,6 +325,40 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag return identifier is "FromServices" or "FromServicesAttribute"; }); + object? keyedServiceKey = null; + var isFromKeyedServices = x.AttributeLists.SelectMany(x => x.Attributes) + .Any(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = name.ToString(); + var result = identifier is "FromKeyedServices" or "FromKeyedServicesAttribute"; + if (result) + { + SemanticModel semanticModel = model; // we can use SemanticModel + if (x.ArgumentList?.Arguments.Count > 0) + { + var argumentExpression = x.ArgumentList.Arguments[0].Expression; + + var constantValue = semanticModel.GetConstantValue(argumentExpression); + if (constantValue.HasValue) + { + keyedServiceKey = constantValue.Value; + } + else if (argumentExpression is TypeOfExpressionSyntax typeOf) + { + var typeInfo = semanticModel.GetTypeInfo(typeOf.Type); + keyedServiceKey = typeInfo.Type; + } + } + } + return result; + }); + var hasArgument = x.AttributeLists.SelectMany(x => x.Attributes) .Any(x => { @@ -360,6 +398,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag IsNullableReference = isNullableReference, IsConsoleAppContext = isConsoleAppContext, IsParams = hasParams, + IsHidden = isHidden, Type = new EquatableTypeSymbol(type.Type!), Location = x.GetLocation(), HasDefaultValue = hasDefault, @@ -368,6 +407,8 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag HasValidation = hasValidation, IsCancellationToken = isCancellationToken, IsFromServices = isFromServices, + IsFromKeyedServices = isFromKeyedServices, + KeyedServiceKey = keyedServiceKey, Aliases = [], Description = "", ArgumentIndex = argumentIndex, @@ -381,6 +422,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag Name = commandName, IsAsync = isAsync, IsVoid = isVoid, + IsHidden = false, // Anonymous lambda don't support attribute. Parameters = parameters, MethodKind = MethodKind.Lambda, Description = "", @@ -472,6 +514,8 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag return null; } + var isHiddenCommand = methodSymbol.GetAttributes().Any(x => x.AttributeClass?.Name == "HiddenAttribute"); + var methodFilters = methodSymbol.GetAttributes() .Where(x => x.AttributeClass?.Name == "ConsoleAppFilterAttribute") .Select(x => @@ -481,7 +525,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag if (filter == null) { - context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConstructor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); return null!; } @@ -493,7 +537,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag return null; } - // validate parametersymbols + // validate parameter symbols if (parameterDescriptions != null) { foreach (var item in parameterDescriptions) @@ -512,10 +556,19 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag { var customParserType = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.AllInterfaces.Any(y => y.Name == "IArgumentParser") ?? false); var hasFromServices = x.GetAttributes().Any(x => x.AttributeClass?.Name == "FromServicesAttribute"); + var hasFromKeyedServices = x.GetAttributes().Any(x => x.AttributeClass?.Name == "FromKeyedServicesAttribute"); var hasArgument = x.GetAttributes().Any(x => x.AttributeClass?.Name == "ArgumentAttribute"); var hasValidation = x.GetAttributes().Any(x => x.AttributeClass?.GetBaseTypes().Any(y => y.Name == "ValidationAttribute") ?? false); var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); var isConsoleAppContext = x.Type!.Name == "ConsoleAppContext"; + var isHiddenParameter = x.GetAttributes().Any(x => x.AttributeClass?.Name == "HiddenAttribute"); + + object? keyedServiceKey = null; + if (hasFromKeyedServices) + { + var attr = x.GetAttributes().First(x => x.AttributeClass?.Name == "FromKeyedServicesAttribute"); + keyedServiceKey = attr.ConstructorArguments[0].Value; + } string description = ""; string[] aliases = []; @@ -547,6 +600,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag IsNullableReference = isNullableReference, IsConsoleAppContext = isConsoleAppContext, IsParams = x.IsParams, + IsHidden = isHiddenParameter, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), Type = new EquatableTypeSymbol(x.Type), HasDefaultValue = x.HasExplicitDefaultValue, @@ -554,6 +608,8 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag CustomParserType = customParserType?.AttributeClass?.ToEquatable(), IsCancellationToken = isCancellationToken, IsFromServices = hasFromServices, + IsFromKeyedServices = hasFromKeyedServices, + KeyedServiceKey = keyedServiceKey, HasValidation = hasValidation, Aliases = aliases, ArgumentIndex = argumentIndex, @@ -567,6 +623,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag Name = commandName, IsAsync = isAsync, IsVoid = isVoid, + IsHidden = isHiddenCommand, Parameters = parameters, MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, Description = summary, diff --git a/src/ConsoleAppFramework/Properties/launchSettings.json b/src/ConsoleAppFramework/Properties/launchSettings.json index 7f7a5b05..11e8a667 100644 --- a/src/ConsoleAppFramework/Properties/launchSettings.json +++ b/src/ConsoleAppFramework/Properties/launchSettings.json @@ -5,4 +5,4 @@ "targetProject": "..\\..\\sandbox\\GeneratorSandbox\\GeneratorSandbox.csproj" } } -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs index 53f937cc..8c4f2f99 100644 --- a/src/ConsoleAppFramework/RoslynExtensions.cs +++ b/src/ConsoleAppFramework/RoslynExtensions.cs @@ -92,9 +92,9 @@ public static Location Clone(this Location location) public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: - // ISymbol.GetDocumentationCommtentXml requirestrue. + // ISymbol.GetDocumentationCommentXml requirestrue. // However, getting the DocumentationCommentTrivia of a SyntaxNode also requires the same condition. - // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse, + // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse, // it becomes None, and the necessary Trivia cannot be obtained. // Therefore, we will attempt to reparse and retrieve it. diff --git a/src/ConsoleAppFramework/SourceBuilder.cs b/src/ConsoleAppFramework/SourceBuilder.cs index e3fb8b9f..478ca34b 100644 --- a/src/ConsoleAppFramework/SourceBuilder.cs +++ b/src/ConsoleAppFramework/SourceBuilder.cs @@ -115,4 +115,4 @@ public void Dispose() { } } -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/SourceGeneratorContexts.cs b/src/ConsoleAppFramework/SourceGeneratorContexts.cs index c1cbba0c..45258410 100644 --- a/src/ConsoleAppFramework/SourceGeneratorContexts.cs +++ b/src/ConsoleAppFramework/SourceGeneratorContexts.cs @@ -2,4 +2,4 @@ readonly record struct ConsoleAppFrameworkGeneratorOptions(bool DisableNamingConversion); -readonly record struct DllReference(bool HasDependencyInjection, bool HasLogging, bool HasConfiguration, bool HasJsonConfiguration, bool HasHost); \ No newline at end of file +readonly record struct DllReference(bool HasDependencyInjection, bool HasLogging, bool HasConfiguration, bool HasJsonConfiguration, bool HasHost); diff --git a/src/ConsoleAppFramework/StringExtensions.cs b/src/ConsoleAppFramework/StringExtensions.cs new file mode 100644 index 00000000..7d1473f4 --- /dev/null +++ b/src/ConsoleAppFramework/StringExtensions.cs @@ -0,0 +1,23 @@ +namespace ConsoleAppFramework; + +internal static class StringExtensions +{ +#if NETSTANDARD2_0 + public static string ReplaceLineEndings(this string input) + { +#pragma warning disable RS1035 + return ReplaceLineEndings(input, Environment.NewLine); +#pragma warning restore RS1035 + } + + public static string ReplaceLineEndings(this string text, string replacementText) + { + text = text.Replace("\r\n", "\n"); + + if (replacementText != "\n") + text = text.Replace("\n", replacementText); + + return text; + } +#endif +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ArrayParseTest.cs similarity index 94% rename from tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs rename to tests/ConsoleAppFramework.GeneratorTests/ArrayParseTest.cs index b5d932e7..4483ea88 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ArrayParseTest.cs @@ -1,8 +1,6 @@ -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests +namespace ConsoleAppFramework.GeneratorTests { - public class ArrayPraseTest(ITestOutputHelper output) + public class ArrayParseTest(ITestOutputHelper output) { VerifyHelper verifier = new VerifyHelper(output, "CAF"); diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 441bfdd2..06e1ad2b 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -1,4 +1,4 @@ -using ConsoleAppFramework; +using ConsoleAppFramework; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Loader; -using Xunit.Abstractions; public static class CSharpGeneratorRunner { @@ -25,7 +24,13 @@ public static void InitializeCompilation() var references = AppDomain.CurrentDomain.GetAssemblies() .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) - .Select(x => MetadataReference.CreateFromFile(x.Location)); + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .Concat([ + MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), // System.Console.dll + MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location), // System.ComponentModel.dll + MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute).Assembly.Location), // System.ComponentModel.DataAnnotations + MetadataReference.CreateFromFile(typeof(System.Text.Json.JsonDocument).Assembly.Location), // System.Text.Json.dll + ]); var compilation = CSharpCompilation.Create("generatortest", references: references, @@ -41,7 +46,7 @@ public static (Compilation, ImmutableArray) RunGenerator([StringSynt { preprocessorSymbols = new[] { "NET8_0_OR_GREATER" }; } - var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12, preprocessorSymbols: preprocessorSymbols); // 12 + var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13, preprocessorSymbols: preprocessorSymbols); // 13 var driver = CSharpGeneratorDriver.Create(new ConsoleAppGenerator()).WithUpdatedParseOptions(parseOptions); if (options != null) @@ -94,7 +99,7 @@ public static (Compilation, ImmutableArray, string) CompileAndExecut public static (string Key, string Reasons)[][] GetIncrementalGeneratorTrackedStepsReasons(string keyPrefixFilter, params string[] sources) { - var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12); // 12 + var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13); // 13 var driver = CSharpGeneratorDriver.Create( [new ConsoleAppGenerator().AsSourceGenerator()], driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)) @@ -144,7 +149,7 @@ public class VerifyHelper(ITestOutputHelper output, string idPrefix) public void Ok([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null) { - output.WriteLine(codeExpr); + output.WriteLine(codeExpr!); var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); foreach (var item in diagnostics) @@ -158,7 +163,7 @@ public void Ok([StringSyntax("C#-test")] string code, [CallerArgumentExpression( public void Verify(int id, [StringSyntax("C#-test")] string code, string diagnosticsCodeSpan, [CallerArgumentExpression("code")] string? codeExpr = null) { - output.WriteLine(codeExpr); + output.WriteLine(codeExpr!); var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); foreach (var item in diagnostics) @@ -176,7 +181,7 @@ public void Verify(int id, [StringSyntax("C#-test")] string code, string diagnos public (string, string)[] Verify([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null) { - output.WriteLine(codeExpr); + output.WriteLine(codeExpr!); var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); OutputGeneratedCode(compilation); @@ -185,9 +190,9 @@ public void Verify(int id, [StringSyntax("C#-test")] string code, string diagnos // Execute and check stdout result - public void Execute([StringSyntax("C#-test")]string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null) + public void Execute([StringSyntax("C#-test")] string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null) { - output.WriteLine(codeExpr); + output.WriteLine(codeExpr!); var (compilation, diagnostics, stdout) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); foreach (var item in diagnostics) @@ -196,12 +201,12 @@ public void Execute([StringSyntax("C#-test")]string code, string args, string ex } OutputGeneratedCode(compilation); - stdout.ShouldBe(expected); + stdout.ShouldBe(expected, StringCompareShould.IgnoreLineEndings); } public string Error([StringSyntax("C#-test")] string code, string args, [CallerArgumentExpression("code")] string? codeExpr = null) { - output.WriteLine(codeExpr); + output.WriteLine(codeExpr!); var (compilation, diagnostics, stdout) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); foreach (var item in diagnostics) @@ -241,4 +246,4 @@ void OutputGeneratedCode(Compilation compilation) output.WriteLine(syntaxTree.ToString()); } } -} \ No newline at end of file +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 311416d1..b32e38c5 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -1,17 +1,11 @@ -using Microsoft.VisualStudio.TestPlatform.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; +namespace ConsoleAppFramework.GeneratorTests; -namespace ConsoleAppFramework.GeneratorTests; - -public class ConsoleAppBuilderTest(ITestOutputHelper output) +public class ConsoleAppBuilderTest(ITestOutputHelper output) : IDisposable { VerifyHelper verifier = new VerifyHelper(output, "CAF"); + public void Dispose() => Environment.ExitCode = 0; + [Fact] public void BuilderRun() { @@ -242,7 +236,7 @@ public void Do() verifier.Execute(code, "nomunomu", "yeah"); } - [Fact] + [Fact] public void CommandAttrWithFilter() { var code = """ @@ -283,4 +277,4 @@ public override Task InvokeAsync(ConsoleAppContext context,CancellationToken can verifier.Execute(code, "nomunomu", "filter1-filter2-command"); } -} \ No newline at end of file +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs index 674270cc..c0438efc 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs @@ -40,4 +40,65 @@ public override Task InvokeAsync(ConsoleAppContext context,CancellationToken can } """, args: "", expected: "12"); } + + [Theory] + [InlineData("--x 1 --y 2", "", "--x 1 --y 2", "")] // no command, no espace + [InlineData("foo --x 1 --y 2", "foo", "--x 1 --y 2", "")] // command, no espace + [InlineData("foo bar --x 1 --y 2", "foo bar", "--x 1 --y 2", "")] // nested command, no espace + [InlineData("--x 1 --y 2 -- abc", "", "--x 1 --y 2", "abc")] // no command, espace + [InlineData("--x 1 --y 2 -- abc def", "", "--x 1 --y 2", "abc def")] // no command, espace2 + [InlineData("foo --x 1 --y 2 -- abc", "foo", "--x 1 --y 2", "abc")] // command, espace + [InlineData("foo --x 1 --y 2 -- abc def", "foo", "--x 1 --y 2", "abc def")] // command, espace2 + [InlineData("foo bar --x 1 --y 2 -- abc", "foo bar", "--x 1 --y 2", "abc")] // nested command, espace + [InlineData("foo bar --x 1 --y 2 -- abc def", "foo bar", "--x 1 --y 2", "abc def")] // nested command, espace2 + public void ArgumentsParseTest(string args, string commandName, string expectedCommandArguments, string expectedEscapedArguments) + { + var argsSpan = args.Split(' ').AsSpan(); + var commandDepth = (commandName == "") ? 0 : (argsSpan.Length - args.Replace(commandName, "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Length); + var escapeIndex = argsSpan.IndexOf("--"); + + var ctx = new ConsoleAppContext2(commandName, argsSpan.ToArray(), null, commandDepth, escapeIndex); + + string.Join(" ", ctx.CommandArguments!).ShouldBe(expectedCommandArguments); + string.Join(" ", ctx.EscapedArguments!).ShouldBe(expectedEscapedArguments); + } + + public class ConsoleAppContext2 + { + public string CommandName { get; } + public string[] Arguments { get; } + public object? State { get; } + + int commandDepth; + int escapeIndex; + + public ReadOnlySpan CommandArguments + { + get => (escapeIndex == -1) + ? Arguments.AsSpan(commandDepth) + : Arguments.AsSpan(commandDepth, escapeIndex - commandDepth); + } + + public ReadOnlySpan EscapedArguments + { + get => (escapeIndex == -1) + ? Array.Empty() + : Arguments.AsSpan(escapeIndex + 1); + } + + public ConsoleAppContext2(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex) + { + this.CommandName = commandName; + this.Arguments = arguments; + this.State = state; + + this.commandDepth = commandDepth; + this.escapeIndex = escapeIndex; + } + + public override string ToString() + { + return string.Join(" ", Arguments); + } + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 68335771..32d7b249 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -1,32 +1,36 @@  - - net8.0 - enable - enable - false - true - Debug;Release - + + Exe + net9.0 + enable + enable + false + true + Debug;Release + - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - - - + + + + + + + + diff --git a/tests/ConsoleAppFramework.GeneratorTests/DITest.cs b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs index 76ab4530..375b8990 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DITest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs @@ -38,4 +38,4 @@ class MyClass(string name) } """, args: "--x 10 --y 20", expected: "foo:10:20"); } -} \ No newline at end of file +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 9facc174..0b145f69 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,6 +1,4 @@ -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; +namespace ConsoleAppFramework.GeneratorTests; public class DiagnosticsTest(ITestOutputHelper output) { diff --git a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs index 46cbcfaf..ab26b3bf 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; +namespace ConsoleAppFramework.GeneratorTests; public class FilterTest(ITestOutputHelper output) { diff --git a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs index 5ad8c783..3df5f4b4 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs @@ -1,3 +1,3 @@ -// CSharpGeneratorRunner.CompileAndExecute uses stdout hook(replace Console.Out) +// CSharpGeneratorRunner.CompileAndExecute uses stdout hook(replace Console.Out) // so can not work in parallel test -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs index 552a584d..3bd24446 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -4,7 +4,6 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Xunit.Abstractions; namespace ConsoleAppFramework.GeneratorTests; @@ -15,7 +14,7 @@ public class HelpTest(ITestOutputHelper output) [Fact] public void Version() { - var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "1.0.0"; + var version = GetEntryAssemblyVersion(); verifier.Execute(code: $$""" ConsoleApp.Run(args, (int x, int y) => { }); @@ -40,7 +39,7 @@ public void Version() [Fact] public void VersionOnBuilder() { - var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "1.0.0"; + var version = GetEntryAssemblyVersion(); verifier.Execute(code: """ var app = ConsoleApp.Create(); @@ -321,4 +320,21 @@ hello my world. """); } + + private static string GetEntryAssemblyVersion() + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion; + + if (version == null) + return "1.0.0"; + + // Trim SourceRevisionId (SourceLink feature is enabled by default when using .NET SDK 8 or later) + var i = version.IndexOf('+'); + if (i != -1) + { + version = version.Substring(0, i); + } + + return version; + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/HiddenAttributeTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HiddenAttributeTest.cs new file mode 100644 index 00000000..7a2c074a --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/HiddenAttributeTest.cs @@ -0,0 +1,121 @@ +namespace ConsoleAppFramework.GeneratorTests; + +public class HiddenAtttributeTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new(output, "CAF"); + + [Fact] + public void VerifyHiddenOptions_Lambda() + { + var code = + """ + ConsoleApp.Run(args, (int x, [Hidden]int y) => { }); + """; + + // Verify Hidden options is not shown on command help. + verifier.Execute(code, args: "--help", expected: + """ + Usage: [options...] [-h|--help] [--version] + + Options: + --x (Required) + + """); + } + + [Fact] + public void VerifyHiddenCommands_Class() + { + var code = + """ + var builder = ConsoleApp.Create(); + builder.Add(); + await builder.RunAsync(args); + + public class Commands + { + [Hidden] + public void Command1() { Console.Write("command1"); } + + public void Command2() { Console.Write("command2"); } + + [Hidden] + public void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); } + } + """; + + // Verify hidden command is not shown on root help commands. + verifier.Execute(code, args: "--help", expected: + """ + Usage: [command] [-h|--help] [--version] + + Commands: + command2 + + """); + + // Verify Hidden command help is shown when explicitly specify command name. + verifier.Execute(code, args: "command1 --help", expected: + """ + Usage: command1 [-h|--help] [--version] + + """); + + verifier.Execute(code, args: "command2 --help", expected: + """ + Usage: command2 [-h|--help] [--version] + + """); + + verifier.Execute(code, args: "command3 --help", expected: + """ + Usage: command3 [options...] [-h|--help] [--version] + + Options: + --x (Required) + + """); + + // Verify commands involations + verifier.Execute(code, args: "command1", "command1"); + verifier.Execute(code, args: "command2", "command2"); + verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2"); + } + + [Fact] + public void VerifyHiddenCommands_LocalFunctions() + { + var code = + """ + var builder = ConsoleApp.Create(); + + builder.Add("", () => { Console.Write("root"); }); + builder.Add("command1", Command1); + builder.Add("command2", Command2); + builder.Add("command3", Command3); + builder.Run(args); + + [Hidden] + static void Command1() { Console.Write("command1"); } + + static void Command2() { Console.Write("command2"); } + + [Hidden] + static void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); } + """; + + verifier.Execute(code, args: "--help", expected: + """ + Usage: [command] [-h|--help] [--version] + + Commands: + command2 + + """); + + // Verify commands can be invoked. + verifier.Execute(code, args: "command1", expected: "command1"); + verifier.Execute(code, args: "command2", expected: "command2"); + verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2"); + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs index 86fa2edb..3b27c70d 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/IncrementalGeneratorTest.cs @@ -677,4 +677,4 @@ public void IncrDual() var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons("ConsoleApp.Builder.", step1, step2); } -} \ No newline at end of file +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs index 550d8197..534cafa0 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; +namespace ConsoleAppFramework.GeneratorTests; public class NameConverterTest(ITestOutputHelper output) { @@ -30,7 +23,7 @@ public void KebabCase() } [Fact] - public void CommmandName() + public void CommandName() { verifier.Execute(""" var builder = ConsoleApp.Create(); diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index b470bbf7..7c3caabe 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -1,18 +1,11 @@ -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; - -public class Test(ITestOutputHelper output) +namespace ConsoleAppFramework.GeneratorTests; + +public class Test(ITestOutputHelper output) : IDisposable { VerifyHelper verifier = new VerifyHelper(output, "CAF"); + public void Dispose() => Environment.ExitCode = 0; + [Fact] public void SyncRun() { @@ -167,4 +160,4 @@ public void Output(string msg = @"\\") ConsoleApp.Run(args, (string msg = @"\\") => Console.Write(msg)); """, "", @"\\"); } -} \ No newline at end of file +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs index 13acb27e..9130658d 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; +namespace ConsoleAppFramework.GeneratorTests; public class SubCommandTest(ITestOutputHelper output) { @@ -29,7 +22,7 @@ public void Zeroargs() builder.Run(args); """; - verifier.Execute(code, "", "root"); + verifier.Execute(code, "", "root"); verifier.Execute(code, "a", "a"); verifier.Execute(code, "a b1", "a b1"); verifier.Execute(code, "a b2", "a b2"); @@ -57,7 +50,7 @@ public void Withargs() builder.Run(args); """; - verifier.Execute(code, "--x 10 --y 20", "root 10 20"); + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); @@ -85,7 +78,7 @@ public void ZeroargsAsync() await builder.RunAsync(args); """; - verifier.Execute(code, "", "root"); + verifier.Execute(code, "", "root"); verifier.Execute(code, "a", "a"); verifier.Execute(code, "a b1", "a b1"); verifier.Execute(code, "a b2", "a b2"); @@ -113,7 +106,7 @@ public void WithargsAsync() await builder.RunAsync(args); """; - verifier.Execute(code, "--x 10 --y 20", "root 10 20"); + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 00000000..25e06788 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,25 @@ + + + + + + true + + + true + + + false + + + + + $(TestingPlatformCommandLineArguments) --xunit-info + + + $(TestingPlatformCommandLineArguments) --results-directory "$(MSBuildThisFileDirectory)TestResults" + + + $(TestingPlatformCommandLineArguments) --ignore-exit-code 8 + +