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;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user