Skip to content

Commit

Permalink
Merge pull request #33 from Cysharp/hotfix/CommandLineOptions
Browse files Browse the repository at this point in the history
Hotfix/command line options
  • Loading branch information
mayuki authored Feb 14, 2020
2 parents 6bcef01 + a9c2912 commit 36e78db
Show file tree
Hide file tree
Showing 15 changed files with 826 additions and 34 deletions.
15 changes: 11 additions & 4 deletions ConsoleAppFramework.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.168
# Visual Studio Version 16
VisualStudioVersion = 16.0.29728.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F399F98-7439-4F05-847B-CC1267B4B7F2}"
EndProject
Expand All @@ -24,9 +24,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".circleci", ".circleci", "{
.circleci\config.yml = .circleci\config.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.Integration.Test", "tests\ConsoleAppFramework.Integration.Test\ConsoleAppFramework.Integration.Test.csproj", "{6A39E146-8CDF-4B04-88ED-395C56A32722}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -62,6 +64,10 @@ Global
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.Build.0 = Release|Any CPU
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -74,6 +80,7 @@ Global
{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A} = {1F399F98-7439-4F05-847B-CC1267B4B7F2}
{2B7CDEFC-3D92-4B72-8898-2494D7B087AD} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}
{6A39E146-8CDF-4B04-88ED-395C56A32722} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C}
Expand Down
3 changes: 3 additions & 0 deletions ConsoleAppFramework.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<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">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cysharp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Konnichiwa/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
8 changes: 4 additions & 4 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,21 @@ public void Hello(
{
```

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

```
> SampleApp.exe help
> SampleApp.exe -help
Usage: SampleApp [options...]

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

`version` command shows `AssemblyInformationalVersion` or `AssemblylVersion`.
`-version` option shows `AssemblyInformationalVersion` or `AssemblylVersion`.

```
> SampleApp.exe version
> SampleApp.exe -version
1.0.0
```

Expand Down
54 changes: 35 additions & 19 deletions src/ConsoleAppFramework/ConsoleAppEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task RunAsync(Type type, string[] args)
{
if (method != null)
{
await SetFailAsync(ctx, "Found two public methods(wihtout command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name);
await SetFailAsync(ctx, "Found more than one public methods(without command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name);
return;
}
method = item; // found single public(non-command) method.
Expand Down Expand Up @@ -205,18 +205,36 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
{
var jsonOption = (JsonSerializerOptions)provider.GetService(typeof(JsonSerializerOptions));

var argumentDictionary = ParseArgument(args, argsOffset);
// Collect option types for parsing command-line arguments.
var optionTypeByOptionName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < parameters.Length; i++)
{
var item = parameters[i];
var option = item.GetCustomAttribute<OptionAttribute>();

optionTypeByOptionName[item.Name] = item.ParameterType;
if (!string.IsNullOrWhiteSpace(option?.ShortName))
{
optionTypeByOptionName[option!.ShortName!] = item.ParameterType;
}
}

var argumentDictionary = ParseArgument(args, argsOffset, optionTypeByOptionName);
invokeArgs = new object[parameters.Length];

for (int i = 0; i < parameters.Length; i++)
{
var item = parameters[i];
var option = item.GetCustomAttribute<OptionAttribute>();
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);
if (option != null && option.Index != -1)
{
value = new OptionParameter { Value = args[argsOffset + i] };
if (argsOffset + i < args.Length)
{
value = new OptionParameter { Value = args[argsOffset + i] };
}
}

if (value.Value != null || argumentDictionary.TryGetValue(item.Name, out value) || argumentDictionary.TryGetValue(option?.ShortName?.TrimStart('-') ?? "", out value))
Expand Down Expand Up @@ -277,7 +295,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
}
catch
{
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation.";
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation.";
return false;
}
}
Expand All @@ -290,7 +308,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
}
catch
{
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation.";
errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation.";
return false;
}
}
Expand Down Expand Up @@ -335,7 +353,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO
return null;
}

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

key = key.TrimStart('-');
if (i >= args.Length)
{
dict.Add(key, new OptionParameter { BooleanSwitch = true }); // Last parameter
break;
}

var value = args[i];
if (value != null && !value.StartsWith("-"))
{
dict.Add(key, new OptionParameter { Value = value });
i++;
}
else
if (optionTypeByName.TryGetValue(key, out var optionType))
{
dict.Add(key, new OptionParameter { BooleanSwitch = true });
if (optionType == typeof(bool))
{
dict.Add(key, new OptionParameter { BooleanSwitch = true });
}
else
{
var value = args[i];
dict.Add(key, new OptionParameter { Value = value });
i++;
}
}
}

Expand Down
15 changes: 10 additions & 5 deletions src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,26 +150,26 @@ IHostBuilder ConfigureEmptyService()
else
{
// override default Help
args = new string[] { "help" };
args = new string[] { "--help" };
}
}
}

if (!hasHelp && args.Length == 1 && TrimEquals(args[0], HelpCommand))
if (!hasHelp && args.Length == 1 && OptionEquals(args[0], HelpCommand))
{
Console.Write(new CommandHelpBuilder().BuildHelpMessage(methods, defaultMethod));
ConfigureEmptyService();
return hostBuilder;
}

if (args.Length == 1 && TrimEquals(args[0], VersionCommand))
if (args.Length == 1 && OptionEquals(args[0], VersionCommand))
{
ShowVersion();
ConfigureEmptyService();
return hostBuilder;
}

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

Expand All @@ -179,7 +179,7 @@ IHostBuilder ConfigureEmptyService()
methodIndex = 1;
}
// command -help
else if (TrimEquals(args[1], HelpCommand))
else if (OptionEquals(args[1], HelpCommand))
{
methodIndex = 0;
}
Expand Down Expand Up @@ -223,6 +223,11 @@ static bool TrimEquals(string arg, string command)
return arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase);
}

static bool OptionEquals(string arg, string command)
{
return arg.StartsWith("-") && arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase);
}

static void ShowVersion()
{
var asm = Assembly.GetEntryAssembly();
Expand Down
4 changes: 4 additions & 0 deletions tests/ConsoleAppFramework.Integration.Test/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using Xunit;

// NOTE: This test project contains integration tests that use `Console.Out` directly. Therefore, the tests must be run sequentially.
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]
25 changes: 25 additions & 0 deletions tests/ConsoleAppFramework.Integration.Test/CaptureConsoleOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.IO;

namespace ConsoleAppFramework.Integration.Test
{
public class CaptureConsoleOutput : IDisposable
{
private readonly TextWriter _originalWriter;
private readonly StringWriter _stringWriter;

public CaptureConsoleOutput()
{
_originalWriter = Console.Out;
_stringWriter = new StringWriter();
Console.SetOut(_stringWriter);
}

public string Output => _stringWriter.ToString();

public void Dispose()
{
Console.SetOut(_originalWriter);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ConsoleAppFramework\ConsoleAppFramework.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 36e78db

Please sign in to comment.