diff --git a/ReadMe.md b/ReadMe.md index f373d5a9..46bbf00b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2,7 +2,7 @@ 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. diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 89c2ed5a..641e3f91 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -2,7 +2,9 @@ Exe - net8.0 + net9.0 + 13 + enable disable true diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 2c0db53f..34b0cde4 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,7 +1,9 @@ #nullable enable using ConsoleAppFramework; +using GeneratorSandbox; using Microsoft.Extensions.DependencyInjection; +using System.Linq; using System.Text.Json; //using Microsoft.Extensions.Configuration; //using Microsoft.Extensions.DependencyInjection; @@ -29,14 +31,30 @@ // services.Configure(configuration.GetSection("Position")); // }); -//app.Add(); -//app.Run(args); -// sc.BuildServiceProvider() -//IServiceProvider ser; -//ser.CreateScope() +args = ["run", "--project", "foo.csproj", "--", "--foo", "100", "--bar", "bazbaz"]; -ConsoleApp.Run(args, () => { }); +// 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); + + + +//ConsoleApp.Run(args, (ConsoleAppContext ctx) => { }); // inject options //public class MyCommand(IOptions options) @@ -115,24 +133,13 @@ public class MyService public class MyCommands { - /// - /// - /// - /// foobarbaz! - [Command("Error1")] - public void Error1(string msg = @"\") + public void Cmd1(int x, int y, ConsoleAppContext ctx) { - Console.WriteLine(msg); } - [Command("Error2")] - public void Error2(string msg = "\\") - { - Console.WriteLine(msg); - } - [Command("Output")] - public void Output(string msg = @"\\") + + public Task Cmd2(int x, int y) { - Console.WriteLine(msg); // 「\」 + return Task.CompletedTask; } } diff --git a/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs b/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs index be56be9f..a67a49d7 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) { diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 1f74ddf6..da0842d0 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -173,7 +173,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 +193,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 +202,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 +230,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 +248,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 +272,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]); }}"; } } } diff --git a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs index f69f1548..cfc8d82c 100644 --- a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs +++ b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs @@ -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) { @@ -329,12 +364,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) { diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 435cd4c2..77331d74 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++) { @@ -113,7 +128,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy 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 +152,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)")) @@ -538,16 +553,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 +580,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 +599,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);"); } } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 441bfdd2..5571fa95 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -41,7 +41,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 +94,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)) @@ -185,7 +185,7 @@ 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); 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..65abe8e4 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable false @@ -11,7 +11,7 @@ - +