From ccbd4872960be65f5e38000f164f7502d053d67b Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 14 May 2020 13:01:59 +0900 Subject: [PATCH] Respect the Index property value of OptionAttribute --- src/ConsoleAppFramework/ConsoleAppEngine.cs | 33 +++++++++--- .../MultipleCommandTest.cs | 53 +++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppEngine.cs b/src/ConsoleAppFramework/ConsoleAppEngine.cs index 2f20404..0e33809 100644 --- a/src/ConsoleAppFramework/ConsoleAppEngine.cs +++ b/src/ConsoleAppFramework/ConsoleAppEngine.cs @@ -227,7 +227,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO } } - var argumentDictionary = ParseArgument(args, argsOffset, optionTypeByOptionName); + var (argumentDictionary, optionByIndex) = ParseArgument(args, argsOffset, optionTypeByOptionName); invokeArgs = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) @@ -237,14 +237,24 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO if (!string.IsNullOrWhiteSpace(option?.ShortName) && char.IsDigit(option!.ShortName, 0)) throw new InvalidOperationException($"Option '{item.Name}' has a short name, but the short name must start with A-Z or a-z."); var value = default(OptionParameter); + + // Indexed arguments (e.g. [Option(0)]) if (option != null && option.Index != -1) { - if (argsOffset + i < args.Length) + if (optionByIndex.Count <= option.Index) { - value = new OptionParameter { Value = args[argsOffset + i] }; + if (!item.HasDefaultValue) + { + throw new InvalidOperationException($"Required argument {option.Index} was not found in specified arguments."); + } + } + else + { + value = optionByIndex[option.Index]; } } + // Keyed options (e.g. -foo -bar ) if (value.Value != null || argumentDictionary.TryGetValue(item.Name, out value) || argumentDictionary.TryGetValue(option?.ShortName?.TrimStart('-') ?? "", out value)) { if (parameters[i].ParameterType == typeof(bool) && value.Value == null) @@ -361,18 +371,20 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO return null; } - static ReadOnlyDictionary ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName) + static (ReadOnlyDictionary OptionByKey, IReadOnlyList OptionByIndex) ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName) { var dict = new Dictionary(args.Length, StringComparer.OrdinalIgnoreCase); + var options = new List(); for (int i = argsOffset; i < args.Length;) { - var key = args[i++]; - if (key is null || !key.StartsWith("-")) + var arg = args[i++]; + if (arg is null || !arg.StartsWith("-")) { + options.Add(new OptionParameter() { Value = arg }); continue; // not key } - key = key.TrimStart('-'); + var key = arg.TrimStart('-'); if (optionTypeByName.TryGetValue(key, out var optionType)) { @@ -387,9 +399,14 @@ static ReadOnlyDictionary ParseArgument(string?[] args, i++; } } + else + { + // not key + options.Add(new OptionParameter() { Value = arg }); + } } - return new ReadOnlyDictionary(dict); + return (new ReadOnlyDictionary(dict), options); } struct OptionParameter diff --git a/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs index 4acf474..b4796b4 100644 --- a/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs +++ b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs @@ -86,6 +86,15 @@ public void OptionAndArg_Option() console.Output.Should().Contain("Hello Cysharp (-128)"); } + [Fact] + public void OptionAndArg_Option_ReverseOrdered() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "-age", "-128", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (-128)"); + } + [Fact] public void OptionAndArg_Help() { @@ -146,6 +155,50 @@ public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase public void Konnichiwa() => Console.WriteLine("Konnichiwa"); } + [Fact] + public void OptionAndArg_Option_MixedOrdered_Default() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "-age", "18", "Hello", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (18)"); + } + + [Fact] + public void OptionAndArg_Option_MixedOrdered_2() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "Hello", "-age", "18", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (18)"); + } + + [Fact] + public void OptionAndArg_Option_MixedOrdered_3() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "Hello", "Cysharp", "-age", "18" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (18)"); + } + + [Fact] + public void OptionAndArg_Option_Mixed_Optional() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "greet" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Konnichiwa Anonymous"); + } + + public class CommandTests_Multiple_OptionAndArg_MixedOrdered : ConsoleAppBase + { + [Command("hello")] + public void Hello([Option(1)]string name, int age, [Option(0)]string greeting) => Console.WriteLine($"{greeting} {name} ({age})"); + [Command("greet")] + public void Greet([Option(0)]string greeting = "Konnichiwa", [Option(0)]string name = "Anonymous") => Console.WriteLine($"{greeting} {name}"); + } + [Fact] public void OptionHelp() {