Enhance option converter and add support for array options

This commit is contained in:
Alexey Golub
2019-06-09 21:57:30 +03:00
parent e0211fc141
commit 63d798977d
18 changed files with 399 additions and 115 deletions

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CliFx.Attributes;
using CliFx.Models;
@@ -8,15 +10,12 @@ namespace CliFx.Tests.Dummy.Commands
[Command("add")]
public class AddCommand : Command
{
[CommandOption("a", IsRequired = true, Description = "Left operand.")]
public double A { get; set; }
[CommandOption("b", IsRequired = true, Description = "Right operand.")]
public double B { get; set; }
[CommandOption("values", 'v', IsRequired = true, Description = "Values.")]
public IReadOnlyList<double> Values { get; set; }
public override ExitCode Execute()
{
var result = A + B;
var result = Values.Sum();
Console.WriteLine(result.ToString(CultureInfo.InvariantCulture));
return ExitCode.Success;

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.Dummy.Commands
[DefaultCommand]
public class DefaultCommand : Command
{
[CommandOption("target", ShortName = 't', Description = "Greeting target.")]
[CommandOption("target", 't', Description = "Greeting target.")]
public string Target { get; set; } = "world";
[CommandOption("enthusiastic", ShortName = 'e', Description = "Whether the greeting should be enthusiastic.")]
[CommandOption('e', Description = "Whether the greeting should be enthusiastic.")]
public bool IsEnthusiastic { get; set; }
public override ExitCode Execute()

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.Dummy.Commands
[Command("log")]
public class LogCommand : Command
{
[CommandOption("value", IsRequired = true, Description = "Value whose logarithm is to be found.")]
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
public double Value { get; set; }
[CommandOption("base", Description = "Logarithm base.")]
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; set; } = 10;
public override ExitCode Execute()

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestObjects;
using NUnit.Framework;
@@ -11,54 +13,172 @@ namespace CliFx.Tests
{
private static IEnumerable<TestCaseData> GetData_ConvertOption()
{
yield return new TestCaseData("value", typeof(string), "value");
yield return new TestCaseData(
new CommandOption("option", "value"),
typeof(string),
"value"
);
yield return new TestCaseData("value", typeof(object), "value");
yield return new TestCaseData(
new CommandOption("option", "value"),
typeof(object),
"value"
);
yield return new TestCaseData("true", typeof(bool), true);
yield return new TestCaseData(
new CommandOption("option", "true"),
typeof(bool),
true
);
yield return new TestCaseData("false", typeof(bool), false);
yield return new TestCaseData(
new CommandOption("option", "false"),
typeof(bool),
false
);
yield return new TestCaseData(null, typeof(bool), true);
yield return new TestCaseData(
new CommandOption("option"),
typeof(bool),
true
);
yield return new TestCaseData("123", typeof(int), 123);
yield return new TestCaseData(
new CommandOption("option", "123"),
typeof(int),
123
);
yield return new TestCaseData("123.45", typeof(double), 123.45);
yield return new TestCaseData(
new CommandOption("option", "123.45"),
typeof(double),
123.45
);
yield return new TestCaseData("28 Apr 1995", typeof(DateTime), new DateTime(1995, 04, 28));
yield return new TestCaseData(
new CommandOption("option", "28 Apr 1995"),
typeof(DateTime),
new DateTime(1995, 04, 28)
);
yield return new TestCaseData("28 Apr 1995", typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28)));
yield return new TestCaseData(
new CommandOption("option", "28 Apr 1995"),
typeof(DateTimeOffset),
new DateTimeOffset(new DateTime(1995, 04, 28))
);
yield return new TestCaseData("00:14:59", typeof(TimeSpan), new TimeSpan(00, 14, 59));
yield return new TestCaseData(
new CommandOption("option", "00:14:59"),
typeof(TimeSpan),
new TimeSpan(00, 14, 59)
);
yield return new TestCaseData("value2", typeof(TestEnum), TestEnum.Value2);
yield return new TestCaseData(
new CommandOption("option", "value2"),
typeof(TestEnum),
TestEnum.Value2
);
yield return new TestCaseData("666", typeof(int?), 666);
yield return new TestCaseData(
new CommandOption("option", "666"),
typeof(int?),
666
);
yield return new TestCaseData(null, typeof(int?), null);
yield return new TestCaseData(
new CommandOption("option"),
typeof(int?),
null
);
yield return new TestCaseData("value3", typeof(TestEnum?), TestEnum.Value3);
yield return new TestCaseData(
new CommandOption("option", "value3"),
typeof(TestEnum?),
TestEnum.Value3
);
yield return new TestCaseData(null, typeof(TestEnum?), null);
yield return new TestCaseData(
new CommandOption("option"),
typeof(TestEnum?),
null
);
yield return new TestCaseData("01:00:00", typeof(TimeSpan?), new TimeSpan(01, 00, 00));
yield return new TestCaseData(
new CommandOption("option", "01:00:00"),
typeof(TimeSpan?),
new TimeSpan(01, 00, 00)
);
yield return new TestCaseData(null, typeof(TimeSpan?), null);
yield return new TestCaseData(
new CommandOption("option"),
typeof(TimeSpan?),
null
);
yield return new TestCaseData("value", typeof(TestStringConstructable), new TestStringConstructable("value"));
yield return new TestCaseData(
new CommandOption("option", "value"),
typeof(TestStringConstructable),
new TestStringConstructable("value")
);
yield return new TestCaseData("value", typeof(TestStringParseable), TestStringParseable.Parse("value"));
yield return new TestCaseData(
new CommandOption("option", "value"),
typeof(TestStringParseable),
TestStringParseable.Parse("value")
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value2"}),
typeof(string[]),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value2"}),
typeof(object[]),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"47", "69"}),
typeof(int[]),
new[] {47, 69}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value3"}),
typeof(TestEnum[]),
new[] {TestEnum.Value1, TestEnum.Value3}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value2"}),
typeof(IEnumerable),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value2"}),
typeof(IEnumerable<string>),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOption("option", new[] {"value1", "value2"}),
typeof(IReadOnlyList<string>),
new[] {"value1", "value2"}
);
}
[Test]
[TestCaseSource(nameof(GetData_ConvertOption))]
public void ConvertOption_Test(string value, Type targetType, object expectedConvertedValue)
public void ConvertOption_Test(CommandOption option, Type targetType, object expectedConvertedValue)
{
// Arrange
var converter = new CommandOptionConverter();
// Act
var convertedValue = converter.ConvertOption(value, targetType);
var convertedValue = converter.ConvertOption(option, targetType);
// Assert
Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue));

View File

@@ -14,96 +14,128 @@ namespace CliFx.Tests
yield return new TestCaseData(
new[] {"--option", "value"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"option", "value"}
new CommandOption("option", "value")
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "--option2", "value2"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"option1", "value1"},
{"option2", "value2"}
new CommandOption("option1", "value1"),
new CommandOption("option2", "value2")
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "value2"},
new CommandOptionSet(new[]
{
new CommandOption("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "--option", "value2"},
new CommandOptionSet(new[]
{
new CommandOption("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"a", "value"}
new CommandOption("a", "value")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-b", "value2"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"a", "value1"},
{"b", "value2"}
new CommandOption("a", "value1"),
new CommandOption("b", "value2")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "value2"},
new CommandOptionSet(new[]
{
new CommandOption("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-a", "value2"},
new CommandOptionSet(new[]
{
new CommandOption("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "-b", "value2"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"option1", "value1"},
{"b", "value2"}
new CommandOption("option1", "value1"),
new CommandOption("b", "value2")
})
);
yield return new TestCaseData(
new[] {"--switch"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"switch", null}
new CommandOption("switch")
})
);
yield return new TestCaseData(
new[] {"--switch1", "--switch2"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"switch1", null},
{"switch2", null}
new CommandOption("switch1"),
new CommandOption("switch2")
})
);
yield return new TestCaseData(
new[] {"-s"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"s", null}
new CommandOption("s")
})
);
yield return new TestCaseData(
new[] {"-a", "-b"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"a", null},
{"b", null}
new CommandOption("a"),
new CommandOption("b")
})
);
yield return new TestCaseData(
new[] {"-ab"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"a", null},
{"b", null}
new CommandOption("a"),
new CommandOption("b")
})
);
yield return new TestCaseData(
new[] {"-ab", "value"},
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"a", null},
{"b", "value"}
new CommandOption("a"),
new CommandOption("b", "value")
})
);
@@ -114,9 +146,9 @@ namespace CliFx.Tests
yield return new TestCaseData(
new[] {"command", "--option", "value"},
new CommandOptionSet("command", new Dictionary<string, string>
new CommandOptionSet("command", new[]
{
{"option", "value"}
new CommandOption("option", "value")
})
);
}
@@ -132,8 +164,17 @@ namespace CliFx.Tests
var optionSet = parser.ParseOptions(commandLineArguments);
// Assert
Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName), nameof(optionSet.CommandName));
Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options), nameof(optionSet.Options));
Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName), "Command name");
Assert.That(optionSet.Options.Count, Is.EqualTo(expectedCommandOptionSet.Options.Count), "Option count");
for (var i = 0; i < optionSet.Options.Count; i++)
{
Assert.That(optionSet.Options[i].Name, Is.EqualTo(expectedCommandOptionSet.Options[i].Name),
$"Option[{i}] name");
Assert.That(optionSet.Options[i].Values, Is.EqualTo(expectedCommandOptionSet.Options[i].Values),
$"Option[{i}] values");
}
}
}
}

View File

@@ -14,36 +14,36 @@ namespace CliFx.Tests
private static IEnumerable<TestCaseData> GetData_ResolveCommand()
{
yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"int", "13"}
new CommandOption("int", "13")
}),
new TestCommand {IntOption = 13}
);
yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"int", "13"},
{"str", "hello world" }
new CommandOption("int", "13"),
new CommandOption("str", "hello world")
}),
new TestCommand { IntOption = 13, StringOption = "hello world"}
new TestCommand {IntOption = 13, StringOption = "hello world"}
);
yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"i", "13"}
new CommandOption("i", "13")
}),
new TestCommand { IntOption = 13 }
new TestCommand {IntOption = 13}
);
yield return new TestCaseData(
new CommandOptionSet("command", new Dictionary<string, string>
new CommandOptionSet("command", new[]
{
{"int", "13"}
new CommandOption("int", "13")
}),
new TestCommand { IntOption = 13 }
new TestCommand {IntOption = 13}
);
}
@@ -80,9 +80,9 @@ namespace CliFx.Tests
yield return new TestCaseData(CommandOptionSet.Empty);
yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string>
new CommandOptionSet(new[]
{
{"str", "hello world"}
new CommandOption("str", "hello world")
})
);
}
@@ -92,7 +92,7 @@ namespace CliFx.Tests
public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet)
{
// Arrange
var commandTypes = new[] { typeof(TestCommand) };
var commandTypes = new[] {typeof(TestCommand)};
var typeProviderMock = new Mock<ITypeProvider>();
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);

View File

@@ -14,9 +14,10 @@ namespace CliFx.Tests
[TestCase("", "Hello world")]
[TestCase("-t .NET", "Hello .NET")]
[TestCase("-e", "Hello world!!!")]
[TestCase("add --a 1 --b 2", "3")]
[TestCase("add --a 2.75 --b 3.6", "6.35")]
[TestCase("log --value 100", "2")]
[TestCase("add -v 1 2", "3")]
[TestCase("add -v 2.75 3.6 4.18", "10.53")]
[TestCase("add -v 4 -v 16", "20")]
[TestCase("log -v 100", "2")]
[TestCase("log --value 256 --base 2", "8")]
public async Task Execute_Test(string arguments, string expectedOutput)
{
@@ -24,9 +25,9 @@ namespace CliFx.Tests
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
// Assert
Assert.That(result.ExitCode, Is.Zero, nameof(result.ExitCode));
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), nameof(result.StandardOutput));
Assert.That(result.StandardError.Trim(), Is.Empty, nameof(result.StandardError));
Assert.That(result.ExitCode, Is.Zero, "Exit code");
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
}
}
}

View File

@@ -7,10 +7,10 @@ namespace CliFx.Tests.TestObjects
[Command("command")]
public class TestCommand : Command
{
[CommandOption("int", ShortName = 'i', IsRequired = true)]
[CommandOption("int", 'i', IsRequired = true)]
public int IntOption { get; set; } = 24;
[CommandOption("str", ShortName = 's')]
[CommandOption("str", 's')]
public string StringOption { get; set; } = "foo bar";
public override ExitCode Execute() => new ExitCode(IntOption, StringOption);

View File

@@ -7,15 +7,31 @@ namespace CliFx.Attributes
{
public string Name { get; }
public char ShortName { get; set; }
public char? ShortName { get; }
public bool IsRequired { get; set; }
public string Description { get; set; }
public CommandOptionAttribute(string name)
public CommandOptionAttribute(string name, char? shortName)
{
Name = name;
ShortName = shortName;
}
public CommandOptionAttribute(string name, char shortName)
: this(name, (char?) shortName)
{
}
public CommandOptionAttribute(string name)
: this(name, null)
{
}
public CommandOptionAttribute(char shortName)
: this(null, shortName)
{
}
}
}

View File

@@ -12,13 +12,13 @@ namespace CliFx.Internal
public string Name { get; }
public char ShortName { get; }
public char? ShortName { get; }
public bool IsRequired { get; }
public string Description { get; }
public CommandOptionProperty(PropertyInfo property, string name, char shortName, bool isRequired, string description)
public CommandOptionProperty(PropertyInfo property, string name, char? shortName, bool isRequired, string description)
{
_property = property;
Name = name;

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace CliFx.Internal
{
@@ -7,6 +9,8 @@ namespace CliFx.Internal
{
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
public static string AsString(this char c) => new string(c, 1);
public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{
var index = s.IndexOf(sub, comparison);
@@ -50,5 +54,30 @@ namespace CliFx.Internal
return false;
}
public static bool IsEnumerable(this Type type) =>
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
public static IReadOnlyList<Type> GetIEnumerableUnderlyingTypes(this Type type)
{
if (type == typeof(IEnumerable))
return new[] {typeof(object)};
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return new[] {type.GetGenericArguments()[0]};
return type.GetInterfaces()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Select(t => t.GetGenericArguments()[0])
.ToArray();
}
public static Array ToNonGenericArray(this ICollection source, Type elementType)
{
var array = Array.CreateInstance(elementType, source.Count);
source.CopyTo(array, 0);
return array;
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace CliFx.Models
{
public class CommandOption
{
public string Name { get; }
public IReadOnlyList<string> Values { get; }
public CommandOption(string name, IReadOnlyList<string> values)
{
Name = name;
Values = values;
}
public CommandOption(string name, string value)
: this(name, new[] {value})
{
}
public CommandOption(string name)
: this(name, new string[0])
{
}
}
}

View File

@@ -8,21 +8,21 @@ namespace CliFx.Models
{
public string CommandName { get; }
public IReadOnlyDictionary<string, string> Options { get; }
public IReadOnlyList<CommandOption> Options { get; }
public CommandOptionSet(string commandName, IReadOnlyDictionary<string, string> options)
public CommandOptionSet(string commandName, IReadOnlyList<CommandOption> options)
{
CommandName = commandName;
Options = options;
}
public CommandOptionSet(IReadOnlyDictionary<string, string> options)
public CommandOptionSet(IReadOnlyList<CommandOption> options)
: this(null, options)
{
}
public CommandOptionSet(string commandName)
: this(commandName, new Dictionary<string, string>())
: this(commandName, new CommandOption[0])
{
}
@@ -30,7 +30,7 @@ namespace CliFx.Models
{
if (Options.Any())
{
var optionsJoined = Options.Select(o => o.Key).JoinToString(", ");
var optionsJoined = Options.Select(o => o.Name).JoinToString(", ");
return !CommandName.IsNullOrWhiteSpace() ? $"{CommandName} / [{optionsJoined}]" : $"[{optionsJoined}]";
}
else
@@ -42,6 +42,6 @@ namespace CliFx.Models
public partial class CommandOptionSet
{
public static CommandOptionSet Empty { get; } = new CommandOptionSet(new Dictionary<string, string>());
public static CommandOptionSet Empty { get; } = new CommandOptionSet(new CommandOption[0]);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Linq;
using CliFx.Internal;
namespace CliFx.Models
{
public static class Extensions
{
public static CommandOption GetOptionOrDefault(this CommandOptionSet set, string name, char? shortName) =>
set.Options.FirstOrDefault(o =>
{
if (!name.IsNullOrWhiteSpace() && string.Equals(o.Name, name, StringComparison.Ordinal))
return true;
if (shortName != null && o.Name.Length == 1 && o.Name.Single() == shortName)
return true;
return false;
});
}
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
@@ -21,7 +22,7 @@ namespace CliFx.Services
{
}
public object ConvertOption(string value, Type targetType)
private object ConvertValue(string value, Type targetType)
{
// String or object
if (targetType == typeof(string) || targetType == typeof(object))
@@ -194,7 +195,7 @@ namespace CliFx.Services
if (value.IsNullOrWhiteSpace())
return null;
return ConvertOption(value, nullableUnderlyingType);
return ConvertValue(value, nullableUnderlyingType);
}
// Has a constructor that accepts a single string
@@ -214,5 +215,26 @@ namespace CliFx.Services
// Unknown type
throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}].");
}
public object ConvertOption(CommandOption option, Type targetType)
{
if (targetType != typeof(string) && targetType.IsEnumerable())
{
var underlyingType = targetType.GetIEnumerableUnderlyingTypes().FirstOrDefault() ?? typeof(object);
if (targetType.IsAssignableFrom(underlyingType.MakeArrayType()))
return option.Values.Select(v => ConvertValue(v, underlyingType)).ToArray().ToNonGenericArray(underlyingType);
throw new CommandOptionConvertException(
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
}
else
{
// Take first value and ignore the rest
var value = option.Values.FirstOrDefault();
return ConvertValue(value, targetType);
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CliFx.Internal;
using CliFx.Models;
@@ -14,7 +14,7 @@ namespace CliFx.Services
string commandName = null;
// Initialize options
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var rawOptions = new Dictionary<string, List<string>>();
// Keep track of the last option's name
string optionName = null;
@@ -28,7 +28,9 @@ namespace CliFx.Services
{
// Extract option name (skip 2 chars)
optionName = commandLineArgument.Substring(2);
options[optionName] = null;
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
}
// Short option name
@@ -36,7 +38,9 @@ namespace CliFx.Services
{
// Extract option name (skip 1 char)
optionName = commandLineArgument.Substring(1);
options[optionName] = null;
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
}
// Multiple stacked short options
@@ -44,8 +48,10 @@ namespace CliFx.Services
{
foreach (var c in commandLineArgument.Substring(1))
{
optionName = c.ToString(CultureInfo.InvariantCulture);
options[optionName] = null;
optionName = c.AsString();
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
}
}
@@ -59,13 +65,13 @@ namespace CliFx.Services
else if (!optionName.IsNullOrWhiteSpace())
{
// ReSharper disable once AssignNullToNotNullAttribute
options[optionName] = commandLineArgument;
rawOptions[optionName].Add(commandLineArgument);
}
isFirstArgument = false;
}
return new CommandOptionSet(commandName, options);
return new CommandOptionSet(commandName, rawOptions.Select(p => new CommandOption(p.Key, p.Value)).ToArray());
}
}
}

View File

@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
@@ -86,18 +86,19 @@ namespace CliFx.Services
// Set command options
foreach (var property in commandType.GetOptionProperties())
{
// If option set contains this property - set value
if (optionSet.Options.TryGetValue(property.Name, out var value) ||
optionSet.Options.TryGetValue(property.ShortName.ToString(CultureInfo.InvariantCulture), out value))
// Get option for this property
var option = optionSet.GetOptionOrDefault(property.Name, property.ShortName);
// If there are any matching options - set value
if (option != null)
{
var convertedValue = _commandOptionConverter.ConvertOption(value, property.Type);
var convertedValue = _commandOptionConverter.ConvertOption(option, property.Type);
property.SetValue(command, convertedValue);
}
// If the property is missing but it's required - throw
else if (property.IsRequired)
{
throw new CommandResolveException(
$"Can't resolve command [{optionSet.CommandName}] because required property [{property.Name}] is not set.");
throw new CommandResolveException($"Can't resolve command because required property [{property.Name}] is not set.");
}
}

View File

@@ -1,9 +1,10 @@
using System;
using CliFx.Models;
namespace CliFx.Services
{
public interface ICommandOptionConverter
{
object ConvertOption(string value, Type targetType);
object ConvertOption(CommandOption option, Type targetType);
}
}