Skip to content

Commit 38c81ed

Browse files
committed
Fix some parsing issue and add integration tests.
1 parent 6bcef01 commit 38c81ed

11 files changed

+636
-23
lines changed

ConsoleAppFramework.sln

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.28307.168
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29728.190
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F399F98-7439-4F05-847B-CC1267B4B7F2}"
77
EndProject
@@ -24,9 +24,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".circleci", ".circleci", "{
2424
.circleci\config.yml = .circleci\config.yml
2525
EndProjectSection
2626
EndProject
27-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}"
27+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}"
2828
EndProject
29-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}"
29+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}"
30+
EndProject
31+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.Integration.Test", "tests\ConsoleAppFramework.Integration.Test\ConsoleAppFramework.Integration.Test.csproj", "{6A39E146-8CDF-4B04-88ED-395C56A32722}"
3032
EndProject
3133
Global
3234
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -62,6 +64,10 @@ Global
6264
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
6365
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
6466
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.Build.0 = Release|Any CPU
67+
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68+
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.Build.0 = Debug|Any CPU
69+
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.ActiveCfg = Release|Any CPU
70+
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.Build.0 = Release|Any CPU
6571
EndGlobalSection
6672
GlobalSection(SolutionProperties) = preSolution
6773
HideSolutionNode = FALSE
@@ -74,6 +80,7 @@ Global
7480
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
7581
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A} = {1F399F98-7439-4F05-847B-CC1267B4B7F2}
7682
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
83+
{6A39E146-8CDF-4B04-88ED-395C56A32722} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
7784
EndGlobalSection
7885
GlobalSection(ExtensibilityGlobals) = postSolution
7986
SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cysharp/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Konnichiwa/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

src/ConsoleAppFramework/ConsoleAppEngine.cs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public async Task RunAsync(Type type, string[] args)
8282
{
8383
if (method != null)
8484
{
85-
await SetFailAsync(ctx, "Found two public methods(wihtout command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name);
85+
await SetFailAsync(ctx, "Found more than one public methods(without command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name);
8686
return;
8787
}
8888
method = item; // found single public(non-command) method.
@@ -205,18 +205,36 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
205205
{
206206
var jsonOption = (JsonSerializerOptions)provider.GetService(typeof(JsonSerializerOptions));
207207

208-
var argumentDictionary = ParseArgument(args, argsOffset);
208+
// Collect option types for parsing command-line arguments.
209+
var optionTypeByOptionName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
210+
for (int i = 0; i < parameters.Length; i++)
211+
{
212+
var item = parameters[i];
213+
var option = item.GetCustomAttribute<OptionAttribute>();
214+
215+
optionTypeByOptionName[item.Name] = item.ParameterType;
216+
if (!string.IsNullOrWhiteSpace(option?.ShortName))
217+
{
218+
optionTypeByOptionName[option!.ShortName!] = item.ParameterType;
219+
}
220+
}
221+
222+
var argumentDictionary = ParseArgument(args, argsOffset, optionTypeByOptionName);
209223
invokeArgs = new object[parameters.Length];
210224

211225
for (int i = 0; i < parameters.Length; i++)
212226
{
213227
var item = parameters[i];
214228
var option = item.GetCustomAttribute<OptionAttribute>();
229+
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.");
215230

216231
var value = default(OptionParameter);
217232
if (option != null && option.Index != -1)
218233
{
219-
value = new OptionParameter { Value = args[argsOffset + i] };
234+
if (argsOffset + i < args.Length)
235+
{
236+
value = new OptionParameter { Value = args[argsOffset + i] };
237+
}
220238
}
221239

222240
if (value.Value != null || argumentDictionary.TryGetValue(item.Name, out value) || argumentDictionary.TryGetValue(option?.ShortName?.TrimStart('-') ?? "", out value))
@@ -277,7 +295,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
277295
}
278296
catch
279297
{
280-
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation.";
298+
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation.";
281299
return false;
282300
}
283301
}
@@ -290,7 +308,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
290308
}
291309
catch
292310
{
293-
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation.";
311+
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation.";
294312
return false;
295313
}
296314
}
@@ -335,7 +353,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
335353
return null;
336354
}
337355

338-
static ReadOnlyDictionary<string, OptionParameter> ParseArgument(string?[] args, int argsOffset)
356+
static ReadOnlyDictionary<string, OptionParameter> ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary<string, Type> optionTypeByName)
339357
{
340358
var dict = new Dictionary<string, OptionParameter>(args.Length, StringComparer.OrdinalIgnoreCase);
341359
for (int i = argsOffset; i < args.Length;)
@@ -347,21 +365,19 @@ static ReadOnlyDictionary<string, OptionParameter> ParseArgument(string?[] args,
347365
}
348366

349367
key = key.TrimStart('-');
350-
if (i >= args.Length)
351-
{
352-
dict.Add(key, new OptionParameter { BooleanSwitch = true }); // Last parameter
353-
break;
354-
}
355368

356-
var value = args[i];
357-
if (value != null && !value.StartsWith("-"))
358-
{
359-
dict.Add(key, new OptionParameter { Value = value });
360-
i++;
361-
}
362-
else
369+
if (optionTypeByName.TryGetValue(key, out var optionType))
363370
{
364-
dict.Add(key, new OptionParameter { BooleanSwitch = true });
371+
if (optionType == typeof(bool))
372+
{
373+
dict.Add(key, new OptionParameter { BooleanSwitch = true });
374+
}
375+
else
376+
{
377+
var value = args[i];
378+
dict.Add(key, new OptionParameter { Value = value });
379+
i++;
380+
}
365381
}
366382
}
367383

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using Xunit;
2+
3+
// NOTE: This test project contains integration tests that use `Console.Out` directly. Therefore, the tests must be run sequentially.
4+
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace ConsoleAppFramework.Integration.Test
5+
{
6+
public class CaptureConsoleOutput : IDisposable
7+
{
8+
private readonly TextWriter _originalWriter;
9+
private readonly StringWriter _stringWriter;
10+
11+
public CaptureConsoleOutput()
12+
{
13+
_originalWriter = Console.Out;
14+
_stringWriter = new StringWriter();
15+
Console.SetOut(_stringWriter);
16+
}
17+
18+
public string Output => _stringWriter.ToString();
19+
20+
public void Dispose()
21+
{
22+
Console.SetOut(_originalWriter);
23+
}
24+
}
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="FluentAssertions" Version="5.10.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
12+
<PackageReference Include="xunit" Version="2.4.0" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
14+
<PackageReference Include="coverlet.collector" Version="1.0.1" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\..\src\ConsoleAppFramework\ConsoleAppFramework.csproj" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using FluentAssertions;
3+
using Microsoft.Extensions.Hosting;
4+
using Xunit;
5+
6+
// ReSharper disable InconsistentNaming
7+
8+
namespace ConsoleAppFramework.Integration.Test
9+
{
10+
public partial class MultipleCommandTest
11+
{
12+
[Fact]
13+
public void NoCommandAttribute()
14+
{
15+
using var console = new CaptureConsoleOutput();
16+
var args = new string[] { };
17+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_NoCommandAttribute>(args);
18+
console.Output.Should().Contain("Found more than one public methods(without command).");
19+
}
20+
21+
public class CommandTests_Multiple_NoCommandAttribute : ConsoleAppBase
22+
{
23+
public void Hello() => Console.WriteLine("Hello");
24+
public void Konnichiwa() => Console.WriteLine("Konnichiwa");
25+
}
26+
27+
[Fact]
28+
public void Commands()
29+
{
30+
using var console = new CaptureConsoleOutput();
31+
var args = new string[] { };
32+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_Commands>(args);
33+
console.Output.Should().Contain("Usage:");
34+
console.Output.Should().Contain("Commands:");
35+
console.Output.Should().Contain("hello");
36+
console.Output.Should().Contain("konnichiwa");
37+
}
38+
39+
[Fact]
40+
public void Commands_UnknownCommand()
41+
{
42+
using var console = new CaptureConsoleOutput();
43+
var args = new string[] { "unknown-command" };
44+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_Commands>(args);
45+
console.Output.Should().Contain("Usage:");
46+
console.Output.Should().Contain("Commands:");
47+
console.Output.Should().Contain("hello");
48+
console.Output.Should().Contain("konnichiwa");
49+
}
50+
51+
[Fact]
52+
public void Commands_UnknownCommand_Help()
53+
{
54+
using var console = new CaptureConsoleOutput();
55+
var args = new string[] { "help", "-foo", "-bar" };
56+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_Commands>(args);
57+
console.Output.Should().Contain("Usage:");
58+
console.Output.Should().Contain("Commands:");
59+
console.Output.Should().Contain("hello");
60+
console.Output.Should().Contain("konnichiwa");
61+
}
62+
63+
public class CommandTests_Multiple_Commands : ConsoleAppBase
64+
{
65+
[Command("hello")]
66+
public void Hello() => Console.WriteLine("Hello");
67+
[Command("konnichiwa")]
68+
public void Konnichiwa() => Console.WriteLine("Konnichiwa");
69+
}
70+
71+
[Fact]
72+
public void OptionAndArg()
73+
{
74+
using var console = new CaptureConsoleOutput();
75+
var args = new string[] { "hello", "Cysharp" };
76+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_OptionAndArg>(args);
77+
console.Output.Should().Contain("Hello Cysharp (18)");
78+
}
79+
80+
[Fact]
81+
public void OptionAndArg_Option()
82+
{
83+
using var console = new CaptureConsoleOutput();
84+
var args = new string[] { "hello", "Cysharp", "-age", "-128" };
85+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_OptionAndArg>(args);
86+
console.Output.Should().Contain("Hello Cysharp (-128)");
87+
}
88+
89+
[Fact]
90+
public void OptionAndArg_HelpOptionLike()
91+
{
92+
using var console = new CaptureConsoleOutput();
93+
var args = new string[] { "hello", "-help", "-age", "-128" };
94+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<CommandTests_Multiple_OptionAndArg>(args);
95+
console.Output.Should().Contain("Hello -help (-128)");
96+
}
97+
98+
public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase
99+
{
100+
[Command("hello")]
101+
public void Hello([Option(0)]string name, int age = 18) => Console.WriteLine($"Hello {name} ({age})");
102+
[Command("konnichiwa")]
103+
public void Konnichiwa() => Console.WriteLine("Konnichiwa");
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)