mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Enhance option converter and add support for array options
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
CliFx/Models/CommandOption.cs
Normal file
27
CliFx/Models/CommandOption.cs
Normal 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])
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
21
CliFx/Models/Extensions.cs
Normal file
21
CliFx/Models/Extensions.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user