5 Commits
0.0.4 ... 0.0.5

Author SHA1 Message Date
Alexey Golub
d1b5107c2c Update version 2019-08-26 20:48:43 +03:00
Alexey Golub
03873d63cd Fix exception when converting option values to array when there's only one value 2019-08-26 20:47:23 +03:00
Alexey Golub
89aba39964 Add extensibility point for injecting custom option converters
Closes #19
2019-08-26 20:10:37 +03:00
Alexey Golub
ab57a103d1 Update benchmarks 2019-08-26 17:20:14 +03:00
Alexey Golub
d0b2ebc061 Update readme 2019-08-25 23:27:19 +03:00
11 changed files with 144 additions and 30 deletions

View File

@@ -8,7 +8,7 @@ namespace CliFx.Benchmarks
[RankColumn]
public class Benchmark
{
private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" };
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[Benchmark(Description = "CliFx", Baseline = true)]
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
@@ -19,16 +19,17 @@ namespace CliFx.Benchmarks
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
// Skipped because this benchmark freezes after a couple of iterations
// Probably wasn't designed to run multiple times in single process execution
//[Benchmark(Description = "CommandLineParser")]
[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser()
{
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand));
var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand));
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
}
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
[Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => clipr.CliParser.Parse<CliprCommand>(Arguments).Execute();
}
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
<PackageReference Include="PowerArgs" Version="3.6.0" />

View File

@@ -0,0 +1,20 @@
using clipr;
namespace CliFx.Benchmarks.Commands
{
public class CliprCommand
{
[NamedArgument('s', "str")]
public string StrOption { get; set; }
[NamedArgument('i', "int")]
public int IntOption { get; set; }
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
public bool BoolOption { get; set; }
public void Execute()
{
}
}
}

View File

@@ -31,6 +31,7 @@ namespace CliFx.Tests
.UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null))
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type))
.UseCommandOptionInputConverter(new CommandOptionInputConverter())
.Build();
}

View File

@@ -214,6 +214,12 @@ namespace CliFx.Tests.Services
new[] {47, 69}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"47"}),
typeof(int[]),
new[] {47}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value3"}),
typeof(TestEnum[]),
@@ -270,6 +276,16 @@ namespace CliFx.Tests.Services
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"123", "456"}),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(TestNonStringParseable)

View File

@@ -25,6 +25,7 @@ namespace CliFx
private string _description;
private IConsole _console;
private ICommandFactory _commandFactory;
private ICommandOptionInputConverter _commandOptionInputConverter;
/// <inheritdoc />
public ICliApplicationBuilder AddCommand(Type commandType)
@@ -108,6 +109,13 @@ namespace CliFx
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
{
_commandOptionInputConverter = converter.GuardNotNull(nameof(converter));
return this;
}
/// <inheritdoc />
public ICliApplication Build()
{
@@ -117,6 +125,7 @@ namespace CliFx
_versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0";
_console = _console ?? new SystemConsole();
_commandFactory = _commandFactory ?? new CommandFactory();
_commandOptionInputConverter = _commandOptionInputConverter ?? new CommandOptionInputConverter();
// Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
@@ -124,7 +133,7 @@ namespace CliFx
return new CliApplication(metadata, configuration,
_console, new CommandInputParser(), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(), new HelpTextRenderer());
_commandFactory, new CommandInitializer(_commandOptionInputConverter), new HelpTextRenderer());
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.0.4</Version>
<Version>0.0.5</Version>
<Company>Tyrrrz</Company>
<Authors>$(Company)</Authors>
<Copyright>Copyright (C) Alexey Golub</Copyright>

View File

@@ -59,6 +59,11 @@ namespace CliFx
/// </summary>
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
/// <summary>
/// Configures application to use specified implementation of <see cref="ICommandOptionInputConverter"/>.
/// </summary>
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter);
/// <summary>
/// Creates an instance of <see cref="ICliApplication"/> using configured parameters.
/// Default values are used in place of parameters that were not specified.

View File

@@ -36,8 +36,13 @@ namespace CliFx.Internal
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
public static Type GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
public static Type GetEnumerableUnderlyingType(this Type type)
{
if (type.IsPrimitive)
return null;
if (type == typeof(IEnumerable))
return typeof(object);

View File

@@ -31,8 +31,13 @@ namespace CliFx.Services
{
}
private object ConvertValue(string value, Type targetType)
/// <summary>
/// Converts a single string value to specified target type.
/// </summary>
protected virtual object ConvertValue(string value, Type targetType)
{
targetType.GuardNotNull(nameof(targetType));
try
{
// String or object
@@ -108,7 +113,7 @@ namespace CliFx.Services
return Enum.Parse(targetType, value, true);
// Nullable
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
if (nullableUnderlyingType != null)
return !value.IsNullOrWhiteSpace() ? ConvertValue(value, nullableUnderlyingType) : null;
@@ -126,48 +131,66 @@ namespace CliFx.Services
var parseMethod = GetStaticParseMethod(targetType);
if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value});
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
}
catch (Exception ex)
{
// Wrap and rethrow exceptions that occur when trying to convert the value
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].", ex);
}
// Throw if we can't find a way to convert the value
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
}
/// <inheritdoc />
public object ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
public virtual object ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
{
optionInput.GuardNotNull(nameof(optionInput));
targetType.GuardNotNull(nameof(targetType));
// Single value
if (optionInput.Values.Count <= 1)
// Get the underlying type of IEnumerable<T> if it's implemented by the target type.
// Ignore string type because it's IEnumerable<T> but we don't treat it as such.
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;
// Convert to a non-enumerable type
if (enumerableUnderlyingType == null)
{
// Throw if provided with more than 1 value
if (optionInput.Values.Count > 1)
{
throw new CliFxException(
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
$"to non-enumerable type [{targetType}].");
}
// Retrieve a single value and convert
var value = optionInput.Values.SingleOrDefault();
return ConvertValue(value, targetType);
}
// Multiple values
// Convert to an enumerable type
else
{
// Determine underlying type of elements inside the target collection type
var underlyingType = targetType.GetEnumerableUnderlyingType() ?? typeof(object);
// Convert values to the underlying enumerable type and cast it to dynamic array
var convertedValues = optionInput.Values
.Select(v => ConvertValue(v, enumerableUnderlyingType))
.ToNonGenericArray(enumerableUnderlyingType);
// Convert values to that type
var convertedValues = optionInput.Values.Select(v => ConvertValue(v, underlyingType)).ToNonGenericArray(underlyingType);
// Get the type of produced array
var convertedValuesType = convertedValues.GetType();
// Assignable from array of values (e.g. T[], IReadOnlyList<T>, IEnumerable<T>)
// Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
if (targetType.IsAssignableFrom(convertedValuesType))
return convertedValues;
// Has a constructor that accepts an array of values (e.g. HashSet<T>, List<T>)
// Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType});
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {convertedValues});
// Throw if we can't find a way to convert the values
throw new CliFxException(
$"Can't convert sequence of values [{optionInput.Values.JoinToString(", ")}] to type [{targetType}].");
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
$"to type [{targetType}].");
}
}
}

View File

@@ -30,6 +30,10 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
- Targets .NET Framework 4.5+ and .NET Standard 2.0+
- No external dependencies
## Screenshots
![](http://www.tyrrrz.me/Projects/CliFx/Images/1.png)
## Argument syntax
This library employs a variation of the GNU command line argument syntax. Because CliFx uses a context-unaware parser, the syntax rules are generally more consistent and intuitive.
@@ -123,6 +127,34 @@ When resolving options, CliFx can convert string values obtained from the comman
If you want to define an option of your own type, the easiest way to do it is to make sure that your type is string-initializable, as explained above.
It is also possible to configure the application to use your own converter, by calling `UseCommandOptionInputConverter` method on `CliApplicationBuilder`.
```c#
var app = new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseCommandOptionInputConverter(new MyConverter())
.Build();
```
The converter class must implement `ICommandOptionInputConverter` but you can also derive from `CommandOptionInputConverter` to extend the default behavior.
```c#
public class MyConverter : CommandOptionInputConverter
{
protected override object ConvertValue(string value, Type targetType)
{
// Custom conversion for MyType
if (targetType == typeof(MyType))
{
// ...
}
// Default behavior for other types
return base.ConvertValue(value, targetType);
}
}
```
### Reporting errors
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
@@ -388,13 +420,12 @@ var app = new CliApplicationBuilder()
## Benchmarks
CliFx has the smallest performance overhead compared to other command line parsers and frameworks.
Below you can see a table comparing execution times of a simple command across different libraries.
Here's how CliFx's execution overhead compares to that of other libraries.
```ini
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.14393.0 (1607/AnniversaryUpdate/Redstone1)
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.14393.3144 (1607/AnniversaryUpdate/Redstone1)
Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
Frequency=3125008 Hz, Resolution=319.9992 ns, Timer=TSC
Frequency=3125011 Hz, Resolution=319.9989 ns, Timer=TSC
.NET Core SDK=2.2.401
[Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
Core : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
@@ -404,10 +435,12 @@ Job=Core Runtime=Core
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|------------------------------------- |----------:|----------:|----------:|------:|--------:|-----:|
| CliFx | 39.47 us | 0.7490 us | 0.9198 us | 1.00 | 0.00 | 1 |
| System.CommandLine | 153.98 us | 0.7112 us | 0.6652 us | 3.90 | 0.09 | 2 |
| McMaster.Extensions.CommandLineUtils | 180.36 us | 3.5893 us | 6.7416 us | 4.59 | 0.16 | 3 |
| PowerArgs | 427.54 us | 6.9006 us | 6.4548 us | 10.82 | 0.26 | 4 |
| CliFx | 31.29 us | 0.6147 us | 0.7774 us | 1.00 | 0.00 | 2 |
| System.CommandLine | 184.44 us | 3.4993 us | 4.0297 us | 5.90 | 0.21 | 4 |
| McMaster.Extensions.CommandLineUtils | 165.50 us | 1.4805 us | 1.3124 us | 5.33 | 0.13 | 3 |
| CommandLineParser | 26.65 us | 0.5530 us | 0.5679 us | 0.85 | 0.03 | 1 |
| PowerArgs | 405.44 us | 7.7133 us | 9.1821 us | 12.96 | 0.47 | 6 |
| Clipr | 220.82 us | 4.4567 us | 4.9536 us | 7.06 | 0.25 | 5 |
## Philosophy