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

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.Dummy.Commands
[DefaultCommand] [DefaultCommand]
public class DefaultCommand : Command public class DefaultCommand : Command
{ {
[CommandOption("target", ShortName = 't', Description = "Greeting target.")] [CommandOption("target", 't', Description = "Greeting target.")]
public string Target { get; set; } = "world"; 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 bool IsEnthusiastic { get; set; }
public override ExitCode Execute() public override ExitCode Execute()

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.Dummy.Commands
[Command("log")] [Command("log")]
public class LogCommand : Command 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; } public double Value { get; set; }
[CommandOption("base", Description = "Logarithm base.")] [CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; set; } = 10; public double Base { get; set; } = 10;
public override ExitCode Execute() public override ExitCode Execute()

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using CliFx.Models;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.TestObjects; using CliFx.Tests.TestObjects;
using NUnit.Framework; using NUnit.Framework;
@@ -11,54 +13,172 @@ namespace CliFx.Tests
{ {
private static IEnumerable<TestCaseData> GetData_ConvertOption() 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] [Test]
[TestCaseSource(nameof(GetData_ConvertOption))] [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 // Arrange
var converter = new CommandOptionConverter(); var converter = new CommandOptionConverter();
// Act // Act
var convertedValue = converter.ConvertOption(value, targetType); var convertedValue = converter.ConvertOption(option, targetType);
// Assert // Assert
Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue)); Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue));

View File

@@ -14,96 +14,128 @@ namespace CliFx.Tests
yield return new TestCaseData( yield return new TestCaseData(
new[] {"--option", "value"}, new[] {"--option", "value"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"option", "value"} new CommandOption("option", "value")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"--option1", "value1", "--option2", "value2"}, new[] {"--option1", "value1", "--option2", "value2"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"option1", "value1"}, new CommandOption("option1", "value1"),
{"option2", "value2"} 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( yield return new TestCaseData(
new[] {"-a", "value"}, new[] {"-a", "value"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"a", "value"} new CommandOption("a", "value")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"-a", "value1", "-b", "value2"}, new[] {"-a", "value1", "-b", "value2"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"a", "value1"}, new CommandOption("a", "value1"),
{"b", "value2"} 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( yield return new TestCaseData(
new[] {"--option1", "value1", "-b", "value2"}, new[] {"--option1", "value1", "-b", "value2"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"option1", "value1"}, new CommandOption("option1", "value1"),
{"b", "value2"} new CommandOption("b", "value2")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"--switch"}, new[] {"--switch"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"switch", null} new CommandOption("switch")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"--switch1", "--switch2"}, new[] {"--switch1", "--switch2"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"switch1", null}, new CommandOption("switch1"),
{"switch2", null} new CommandOption("switch2")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"-s"}, new[] {"-s"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"s", null} new CommandOption("s")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"-a", "-b"}, new[] {"-a", "-b"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"a", null}, new CommandOption("a"),
{"b", null} new CommandOption("b")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"-ab"}, new[] {"-ab"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"a", null}, new CommandOption("a"),
{"b", null} new CommandOption("b")
}) })
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {"-ab", "value"}, new[] {"-ab", "value"},
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"a", null}, new CommandOption("a"),
{"b", "value"} new CommandOption("b", "value")
}) })
); );
@@ -114,9 +146,9 @@ namespace CliFx.Tests
yield return new TestCaseData( yield return new TestCaseData(
new[] {"command", "--option", "value"}, 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); var optionSet = parser.ParseOptions(commandLineArguments);
// Assert // Assert
Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName), nameof(optionSet.CommandName)); Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName), "Command name");
Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options), nameof(optionSet.Options)); 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() private static IEnumerable<TestCaseData> GetData_ResolveCommand()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"int", "13"} new CommandOption("int", "13")
}), }),
new TestCommand {IntOption = 13} new TestCommand {IntOption = 13}
); );
yield return new TestCaseData( yield return new TestCaseData(
new CommandOptionSet(new Dictionary<string, string> new CommandOptionSet(new[]
{ {
{"int", "13"}, new CommandOption("int", "13"),
{"str", "hello world" } new CommandOption("str", "hello world")
}), }),
new TestCommand { IntOption = 13, StringOption = "hello world"} new TestCommand {IntOption = 13, StringOption = "hello world"}
); );
yield return new TestCaseData( 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( 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(CommandOptionSet.Empty);
yield return new TestCaseData( 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) public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet)
{ {
// Arrange // Arrange
var commandTypes = new[] { typeof(TestCommand) }; var commandTypes = new[] {typeof(TestCommand)};
var typeProviderMock = new Mock<ITypeProvider>(); var typeProviderMock = new Mock<ITypeProvider>();
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes); typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);

View File

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

View File

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

View File

@@ -7,15 +7,31 @@ namespace CliFx.Attributes
{ {
public string Name { get; } public string Name { get; }
public char ShortName { get; set; } public char? ShortName { get; }
public bool IsRequired { get; set; } public bool IsRequired { get; set; }
public string Description { get; set; } public string Description { get; set; }
public CommandOptionAttribute(string name) public CommandOptionAttribute(string name, char? shortName)
{ {
Name = name; 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 string Name { get; }
public char ShortName { get; } public char? ShortName { get; }
public bool IsRequired { get; } public bool IsRequired { get; }
public string Description { 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; _property = property;
Name = name; Name = name;

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace CliFx.Internal namespace CliFx.Internal
{ {
@@ -7,6 +9,8 @@ namespace CliFx.Internal
{ {
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); 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) public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{ {
var index = s.IndexOf(sub, comparison); var index = s.IndexOf(sub, comparison);
@@ -50,5 +54,30 @@ namespace CliFx.Internal
return false; 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 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; CommandName = commandName;
Options = options; Options = options;
} }
public CommandOptionSet(IReadOnlyDictionary<string, string> options) public CommandOptionSet(IReadOnlyList<CommandOption> options)
: this(null, options) : this(null, options)
{ {
} }
public CommandOptionSet(string commandName) 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()) 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}]"; return !CommandName.IsNullOrWhiteSpace() ? $"{CommandName} / [{optionsJoined}]" : $"[{optionsJoined}]";
} }
else else
@@ -42,6 +42,6 @@ namespace CliFx.Models
public partial class CommandOptionSet 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 System.Reflection;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services 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 // String or object
if (targetType == typeof(string) || targetType == typeof(object)) if (targetType == typeof(string) || targetType == typeof(object))
@@ -194,7 +195,7 @@ namespace CliFx.Services
if (value.IsNullOrWhiteSpace()) if (value.IsNullOrWhiteSpace())
return null; return null;
return ConvertOption(value, nullableUnderlyingType); return ConvertValue(value, nullableUnderlyingType);
} }
// Has a constructor that accepts a single string // Has a constructor that accepts a single string
@@ -214,5 +215,26 @@ namespace CliFx.Services
// Unknown type // Unknown type
throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}]."); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Linq;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
@@ -14,7 +14,7 @@ namespace CliFx.Services
string commandName = null; string commandName = null;
// Initialize options // 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 // Keep track of the last option's name
string optionName = null; string optionName = null;
@@ -28,7 +28,9 @@ namespace CliFx.Services
{ {
// Extract option name (skip 2 chars) // Extract option name (skip 2 chars)
optionName = commandLineArgument.Substring(2); optionName = commandLineArgument.Substring(2);
options[optionName] = null;
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
} }
// Short option name // Short option name
@@ -36,7 +38,9 @@ namespace CliFx.Services
{ {
// Extract option name (skip 1 char) // Extract option name (skip 1 char)
optionName = commandLineArgument.Substring(1); optionName = commandLineArgument.Substring(1);
options[optionName] = null;
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
} }
// Multiple stacked short options // Multiple stacked short options
@@ -44,8 +48,10 @@ namespace CliFx.Services
{ {
foreach (var c in commandLineArgument.Substring(1)) foreach (var c in commandLineArgument.Substring(1))
{ {
optionName = c.ToString(CultureInfo.InvariantCulture); optionName = c.AsString();
options[optionName] = null;
if (rawOptions.GetValueOrDefault(optionName) == null)
rawOptions[optionName] = new List<string>();
} }
} }
@@ -59,13 +65,13 @@ namespace CliFx.Services
else if (!optionName.IsNullOrWhiteSpace()) else if (!optionName.IsNullOrWhiteSpace())
{ {
// ReSharper disable once AssignNullToNotNullAttribute // ReSharper disable once AssignNullToNotNullAttribute
options[optionName] = commandLineArgument; rawOptions[optionName].Add(commandLineArgument);
} }
isFirstArgument = false; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -86,18 +86,19 @@ namespace CliFx.Services
// Set command options // Set command options
foreach (var property in commandType.GetOptionProperties()) foreach (var property in commandType.GetOptionProperties())
{ {
// If option set contains this property - set value // Get option for this property
if (optionSet.Options.TryGetValue(property.Name, out var value) || var option = optionSet.GetOptionOrDefault(property.Name, property.ShortName);
optionSet.Options.TryGetValue(property.ShortName.ToString(CultureInfo.InvariantCulture), out value))
// 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); property.SetValue(command, convertedValue);
} }
// If the property is missing but it's required - throw // If the property is missing but it's required - throw
else if (property.IsRequired) else if (property.IsRequired)
{ {
throw new CommandResolveException( throw new CommandResolveException($"Can't resolve command because required property [{property.Name}] is not set.");
$"Can't resolve command [{optionSet.CommandName}] because required property [{property.Name}] is not set.");
} }
} }

View File

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