Skip to content

Commit 36e78db

Browse files
authored
Merge pull request #33 from Cysharp/hotfix/CommandLineOptions
Hotfix/command line options
2 parents 6bcef01 + a9c2912 commit 36e78db

15 files changed

+826
-34
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>

ReadMe.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,21 @@ public void Hello(
5858
{
5959
```
6060

61-
`help` command(or no argument to pass) shows there detail. This help format is same as `dotnet` command.
61+
`-help` option (or no argument to pass) shows there detail. This help format is same as `dotnet` command.
6262

6363
```
64-
> SampleApp.exe help
64+
> SampleApp.exe -help
6565
Usage: SampleApp [options...]
6666

6767
Options:
6868
-n, -name <String> name of send user. (Required)
6969
-r, -repeat <Int32> repeat count. (Default: 3)
7070
```
7171

72-
`version` command shows `AssemblyInformationalVersion` or `AssemblylVersion`.
72+
`-version` option shows `AssemblyInformationalVersion` or `AssemblylVersion`.
7373

7474
```
75-
> SampleApp.exe version
75+
> SampleApp.exe -version
7676
1.0.0
7777
```
7878

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

src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,26 +150,26 @@ IHostBuilder ConfigureEmptyService()
150150
else
151151
{
152152
// override default Help
153-
args = new string[] { "help" };
153+
args = new string[] { "--help" };
154154
}
155155
}
156156
}
157157

158-
if (!hasHelp && args.Length == 1 && TrimEquals(args[0], HelpCommand))
158+
if (!hasHelp && args.Length == 1 && OptionEquals(args[0], HelpCommand))
159159
{
160160
Console.Write(new CommandHelpBuilder().BuildHelpMessage(methods, defaultMethod));
161161
ConfigureEmptyService();
162162
return hostBuilder;
163163
}
164164

165-
if (args.Length == 1 && TrimEquals(args[0], VersionCommand))
165+
if (args.Length == 1 && OptionEquals(args[0], VersionCommand))
166166
{
167167
ShowVersion();
168168
ConfigureEmptyService();
169169
return hostBuilder;
170170
}
171171

172-
if (args.Length == 2 && methods.Length != 1)
172+
if (args.Length == 2 && methods.Length > 0 && defaultMethod == null)
173173
{
174174
int methodIndex = -1;
175175

@@ -179,7 +179,7 @@ IHostBuilder ConfigureEmptyService()
179179
methodIndex = 1;
180180
}
181181
// command -help
182-
else if (TrimEquals(args[1], HelpCommand))
182+
else if (OptionEquals(args[1], HelpCommand))
183183
{
184184
methodIndex = 0;
185185
}
@@ -223,6 +223,11 @@ static bool TrimEquals(string arg, string command)
223223
return arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase);
224224
}
225225

226+
static bool OptionEquals(string arg, string command)
227+
{
228+
return arg.StartsWith("-") && arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase);
229+
}
230+
226231
static void ShowVersion()
227232
{
228233
var asm = Assembly.GetEntryAssembly();
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.0</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>

0 commit comments

Comments
 (0)