diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
new file mode 100644
index 0000000..dcae554
--- /dev/null
+++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net45
+ latest
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/AddCommand.cs b/CliFx.Tests.Dummy/Commands/AddCommand.cs
new file mode 100644
index 0000000..e0649f5
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/AddCommand.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using CliFx.Attributes;
+using CliFx.Models;
+
+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; }
+
+ public override ExitCode Execute()
+ {
+ var result = A + B;
+ Console.WriteLine(result.ToString(CultureInfo.InvariantCulture));
+
+ return ExitCode.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs
new file mode 100644
index 0000000..69b7f46
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text;
+using CliFx.Attributes;
+using CliFx.Models;
+
+namespace CliFx.Tests.Dummy.Commands
+{
+ [DefaultCommand]
+ public class DefaultCommand : Command
+ {
+ [CommandOption("target", ShortName = 't', Description = "Greeting target.")]
+ public string Target { get; set; } = "world";
+
+ [CommandOption("enthusiastic", ShortName = 'e', Description = "Whether the greeting should be enthusiastic.")]
+ public bool IsEnthusiastic { get; set; }
+
+ public override ExitCode Execute()
+ {
+ var buffer = new StringBuilder();
+
+ buffer.Append("Hello ").Append(Target);
+
+ if (IsEnthusiastic)
+ buffer.Append("!!!");
+
+ Console.WriteLine(buffer.ToString());
+
+ return ExitCode.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs
new file mode 100644
index 0000000..38532e6
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using CliFx.Attributes;
+using CliFx.Models;
+
+namespace CliFx.Tests.Dummy.Commands
+{
+ [Command("log")]
+ public class LogCommand : Command
+ {
+ [CommandOption("value", IsRequired = true, Description = "Value whose logarithm is to be found.")]
+ public double Value { get; set; }
+
+ [CommandOption("base", Description = "Logarithm base.")]
+ public double Base { get; set; } = 10;
+
+ public override ExitCode Execute()
+ {
+ var result = Math.Log(Value, Base);
+ Console.WriteLine(result.ToString(CultureInfo.InvariantCulture));
+
+ return ExitCode.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Program.cs b/CliFx.Tests.Dummy/Program.cs
new file mode 100644
index 0000000..c76bc5e
--- /dev/null
+++ b/CliFx.Tests.Dummy/Program.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace CliFx.Tests.Dummy
+{
+ public static class Program
+ {
+ public static Task Main(string[] args) => new CliApplication().RunAsync(args);
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs
new file mode 100644
index 0000000..e61e17e
--- /dev/null
+++ b/CliFx.Tests/CliApplicationTests.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using CliFx.Services;
+using CliFx.Tests.TestObjects;
+using Moq;
+using NUnit.Framework;
+
+namespace CliFx.Tests
+{
+ [TestFixture]
+ public class CliApplicationTests
+ {
+ [Test]
+ public async Task RunAsync_Test()
+ {
+ // Arrange
+ var command = new TestCommand();
+ var expectedExitCode = await command.ExecuteAsync();
+
+ var commandResolverMock = new Mock();
+ commandResolverMock.Setup(m => m.ResolveCommand(It.IsAny>())).Returns(command);
+ var commandResolver = commandResolverMock.Object;
+
+ var application = new CliApplication(commandResolver);
+
+ // Act
+ var exitCodeValue = await application.RunAsync();
+
+ // Assert
+ Assert.That(exitCodeValue, Is.EqualTo(expectedExitCode.Value));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj
new file mode 100644
index 0000000..4e246c7
--- /dev/null
+++ b/CliFx.Tests/CliFx.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net45
+ false
+ true
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliFx.Tests/CommandOptionConverterTests.cs b/CliFx.Tests/CommandOptionConverterTests.cs
new file mode 100644
index 0000000..0d90c58
--- /dev/null
+++ b/CliFx.Tests/CommandOptionConverterTests.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using CliFx.Services;
+using CliFx.Tests.TestObjects;
+using NUnit.Framework;
+
+namespace CliFx.Tests
+{
+ [TestFixture]
+ public class CommandOptionConverterTests
+ {
+ private static IEnumerable GetData_ConvertOption()
+ {
+ yield return new TestCaseData("value", typeof(string), "value")
+ .SetName("To string");
+
+ yield return new TestCaseData("value", typeof(object), "value")
+ .SetName("To object");
+
+ yield return new TestCaseData("true", typeof(bool), true)
+ .SetName("To bool (true)");
+
+ yield return new TestCaseData("false", typeof(bool), false)
+ .SetName("To bool (false)");
+
+ yield return new TestCaseData(null, typeof(bool), true)
+ .SetName("To bool (switch)");
+
+ yield return new TestCaseData("123", typeof(int), 123)
+ .SetName("To int");
+
+ yield return new TestCaseData("123.45", typeof(double), 123.45)
+ .SetName("To double");
+
+ yield return new TestCaseData("28 Apr 1995", typeof(DateTime), new DateTime(1995, 04, 28))
+ .SetName("To DateTime");
+
+ yield return new TestCaseData("28 Apr 1995", typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28)))
+ .SetName("To DateTimeOffset");
+
+ yield return new TestCaseData("00:14:59", typeof(TimeSpan), new TimeSpan(00, 14, 59))
+ .SetName("To TimeSpan");
+
+ yield return new TestCaseData("value2", typeof(TestEnum), TestEnum.Value2)
+ .SetName("To enum");
+
+ yield return new TestCaseData("666", typeof(int?), 666)
+ .SetName("To int? (with value)");
+
+ yield return new TestCaseData(null, typeof(int?), null)
+ .SetName("To int? (no value)");
+
+ yield return new TestCaseData("value3", typeof(TestEnum?), TestEnum.Value3)
+ .SetName("To enum? (with value)");
+
+ yield return new TestCaseData(null, typeof(TestEnum?), null)
+ .SetName("To enum? (no value)");
+
+ yield return new TestCaseData("01:00:00", typeof(TimeSpan?), new TimeSpan(01, 00, 00))
+ .SetName("To TimeSpan? (with value)");
+
+ yield return new TestCaseData(null, typeof(TimeSpan?), null)
+ .SetName("To TimeSpan? (no value)");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(GetData_ConvertOption))]
+ public void ConvertOption_Test(string value, Type targetType, object expectedConvertedValue)
+ {
+ // Arrange
+ var converter = new CommandOptionConverter();
+
+ // Act
+ var convertedValue = converter.ConvertOption(value, targetType);
+
+ // Assert
+ Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue));
+
+ if (convertedValue != null)
+ Assert.That(convertedValue, Is.AssignableTo(targetType));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/CommandOptionParserTests.cs b/CliFx.Tests/CommandOptionParserTests.cs
new file mode 100644
index 0000000..cc88b11
--- /dev/null
+++ b/CliFx.Tests/CommandOptionParserTests.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using CliFx.Models;
+using CliFx.Services;
+using NUnit.Framework;
+
+namespace CliFx.Tests
+{
+ [TestFixture]
+ public class CommandOptionParserTests
+ {
+ private static IEnumerable GetData_ParseOptions()
+ {
+ yield return new TestCaseData(
+ new string[0],
+ CommandOptionSet.Empty
+ ).SetName("No arguments");
+
+ yield return new TestCaseData(
+ new[] {"--argument", "value"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"argument", "value"}
+ })
+ ).SetName("Single argument");
+
+ yield return new TestCaseData(
+ new[] {"--argument1", "value1", "--argument2", "value2", "--argument3", "value3"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"argument1", "value1"},
+ {"argument2", "value2"},
+ {"argument3", "value3"}
+ })
+ ).SetName("Multiple arguments");
+
+ yield return new TestCaseData(
+ new[] {"-a", "value"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"a", "value"}
+ })
+ ).SetName("Single short argument");
+
+ yield return new TestCaseData(
+ new[] {"-a", "value1", "-b", "value2", "-c", "value3"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"a", "value1"},
+ {"b", "value2"},
+ {"c", "value3"}
+ })
+ ).SetName("Multiple short arguments");
+
+ yield return new TestCaseData(
+ new[] {"--argument1", "value1", "-b", "value2", "--argument3", "value3"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"argument1", "value1"},
+ {"b", "value2"},
+ {"argument3", "value3"}
+ })
+ ).SetName("Multiple mixed arguments");
+
+ yield return new TestCaseData(
+ new[] {"--switch"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"switch", null}
+ })
+ ).SetName("Single switch");
+
+ yield return new TestCaseData(
+ new[] {"--switch1", "--switch2", "--switch3"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"switch1", null},
+ {"switch2", null},
+ {"switch3", null}
+ })
+ ).SetName("Multiple switches");
+
+ yield return new TestCaseData(
+ new[] {"-s"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"s", null}
+ })
+ ).SetName("Single short switch");
+
+ yield return new TestCaseData(
+ new[] {"-a", "-b", "-c"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"a", null},
+ {"b", null},
+ {"c", null}
+ })
+ ).SetName("Multiple short switches");
+
+ yield return new TestCaseData(
+ new[] {"-abc"},
+ new CommandOptionSet(new Dictionary
+ {
+ {"a", null},
+ {"b", null},
+ {"c", null}
+ })
+ ).SetName("Multiple stacked short switches");
+
+ yield return new TestCaseData(
+ new[] {"command"},
+ new CommandOptionSet("command")
+ ).SetName("No arguments (with command name)");
+
+ yield return new TestCaseData(
+ new[] {"command", "--argument", "value"},
+ new CommandOptionSet("command", new Dictionary
+ {
+ {"argument", "value"}
+ })
+ ).SetName("Single argument (with command name)");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(GetData_ParseOptions))]
+ public void ParseOptions_Test(IReadOnlyList commandLineArguments, CommandOptionSet expectedCommandOptionSet)
+ {
+ // Arrange
+ var parser = new CommandOptionParser();
+
+ // Act
+ var optionSet = parser.ParseOptions(commandLineArguments);
+
+ // Assert
+ Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName));
+ Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/CommandResolverTests.cs b/CliFx.Tests/CommandResolverTests.cs
new file mode 100644
index 0000000..8ec5f2c
--- /dev/null
+++ b/CliFx.Tests/CommandResolverTests.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using CliFx.Exceptions;
+using CliFx.Models;
+using CliFx.Services;
+using CliFx.Tests.TestObjects;
+using Moq;
+using NUnit.Framework;
+
+namespace CliFx.Tests
+{
+ [TestFixture]
+ public class CommandResolverTests
+ {
+ private static IEnumerable GetData_ResolveCommand()
+ {
+ yield return new TestCaseData(
+ new CommandOptionSet(new Dictionary
+ {
+ {"int", "13"}
+ }),
+ new TestCommand {IntOption = 13}
+ ).SetName("Single option");
+
+ yield return new TestCaseData(
+ new CommandOptionSet(new Dictionary
+ {
+ {"int", "13"},
+ {"str", "hello world" }
+ }),
+ new TestCommand { IntOption = 13, StringOption = "hello world"}
+ ).SetName("Multiple options");
+
+ yield return new TestCaseData(
+ new CommandOptionSet(new Dictionary
+ {
+ {"i", "13"}
+ }),
+ new TestCommand { IntOption = 13 }
+ ).SetName("Single short option");
+
+ yield return new TestCaseData(
+ new CommandOptionSet("command", new Dictionary
+ {
+ {"int", "13"}
+ }),
+ new TestCommand { IntOption = 13 }
+ ).SetName("Single option (with command name)");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(GetData_ResolveCommand))]
+ public void ResolveCommand_Test(CommandOptionSet commandOptionSet, TestCommand expectedCommand)
+ {
+ // Arrange
+ var commandTypes = new[] {typeof(TestCommand)};
+
+ var typeProviderMock = new Mock();
+ typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
+ var typeProvider = typeProviderMock.Object;
+
+ var optionParserMock = new Mock();
+ optionParserMock.Setup(m => m.ParseOptions(It.IsAny>())).Returns(commandOptionSet);
+ var optionParser = optionParserMock.Object;
+
+ var optionConverter = new CommandOptionConverter();
+
+ var resolver = new CommandResolver(typeProvider, optionParser, optionConverter);
+
+ // Act
+ var command = resolver.ResolveCommand() as TestCommand;
+
+ // Assert
+ Assert.That(command, Is.Not.Null);
+ Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
+ Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
+ }
+
+ private static IEnumerable GetData_ResolveCommand_IsRequired()
+ {
+ yield return new TestCaseData(
+ CommandOptionSet.Empty
+ ).SetName("No options");
+
+ yield return new TestCaseData(
+ new CommandOptionSet(new Dictionary
+ {
+ {"str", "hello world"}
+ })
+ ).SetName("Required option is not set");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(GetData_ResolveCommand_IsRequired))]
+ public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet)
+ {
+ // Arrange
+ var commandTypes = new[] { typeof(TestCommand) };
+
+ var typeProviderMock = new Mock();
+ typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
+ var typeProvider = typeProviderMock.Object;
+
+ var optionParserMock = new Mock();
+ optionParserMock.Setup(m => m.ParseOptions(It.IsAny>())).Returns(commandOptionSet);
+ var optionParser = optionParserMock.Object;
+
+ var optionConverter = new CommandOptionConverter();
+
+ var resolver = new CommandResolver(typeProvider, optionParser, optionConverter);
+
+ // Act & Assert
+ Assert.Throws(() => resolver.ResolveCommand());
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs
new file mode 100644
index 0000000..0020d01
--- /dev/null
+++ b/CliFx.Tests/DummyTests.cs
@@ -0,0 +1,32 @@
+using System.IO;
+using System.Threading.Tasks;
+using CliWrap;
+using NUnit.Framework;
+
+namespace CliFx.Tests
+{
+ [TestFixture]
+ public class DummyTests
+ {
+ private string DummyFilePath => Path.Combine(TestContext.CurrentContext.TestDirectory, "CliFx.Tests.Dummy.exe");
+
+ [Test]
+ [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("log --value 256 --base 2", "8")]
+ public async Task Execute_Test(string arguments, string expectedOutput)
+ {
+ // Act
+ var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
+
+ // Assert
+ Assert.That(result.ExitCode, Is.Zero);
+ Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput));
+ Assert.That(result.StandardError.Trim(), Is.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/TestObjects/TestCommand.cs b/CliFx.Tests/TestObjects/TestCommand.cs
new file mode 100644
index 0000000..f2d7188
--- /dev/null
+++ b/CliFx.Tests/TestObjects/TestCommand.cs
@@ -0,0 +1,18 @@
+using CliFx.Attributes;
+using CliFx.Models;
+
+namespace CliFx.Tests.TestObjects
+{
+ [DefaultCommand]
+ [Command("command")]
+ public class TestCommand : Command
+ {
+ [CommandOption("int", ShortName = 'i', IsRequired = true)]
+ public int IntOption { get; set; } = 24;
+
+ [CommandOption("str", ShortName = 's')]
+ public string StringOption { get; set; } = "foo bar";
+
+ public override ExitCode Execute() => new ExitCode(IntOption, StringOption);
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/TestObjects/TestEnum.cs b/CliFx.Tests/TestObjects/TestEnum.cs
new file mode 100644
index 0000000..6035f37
--- /dev/null
+++ b/CliFx.Tests/TestObjects/TestEnum.cs
@@ -0,0 +1,9 @@
+namespace CliFx.Tests.TestObjects
+{
+ public enum TestEnum
+ {
+ Value1,
+ Value2,
+ Value3
+ }
+}
\ No newline at end of file
diff --git a/CliFx.sln b/CliFx.sln
new file mode 100644
index 0000000..6410a54
--- /dev/null
+++ b/CliFx.sln
@@ -0,0 +1,65 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28803.352
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj", "{C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x64.Build.0 = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x86.Build.0 = Debug|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x64.ActiveCfg = Release|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x64.Build.0 = Release|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x86.ActiveCfg = Release|Any CPU
+ {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x86.Build.0 = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x64.Build.0 = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x86.Build.0 = Debug|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.ActiveCfg = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU
+ {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU
+ {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6ACC950B-5F93-429C-A204-6315A92AD3A1}
+ EndGlobalSection
+EndGlobal
diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs
new file mode 100644
index 0000000..dbccc97
--- /dev/null
+++ b/CliFx/Attributes/CommandAttribute.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace CliFx.Attributes
+{
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public class CommandAttribute : Attribute
+ {
+ public string Name { get; }
+
+ public CommandAttribute(string name)
+ {
+ Name = name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs
new file mode 100644
index 0000000..67f49d0
--- /dev/null
+++ b/CliFx/Attributes/CommandOptionAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace CliFx.Attributes
+{
+ [AttributeUsage(AttributeTargets.Property)]
+ public class CommandOptionAttribute : Attribute
+ {
+ public string Name { get; }
+
+ public char ShortName { get; set; }
+
+ public bool IsRequired { get; set; }
+
+ public string Description { get; set; }
+
+ public CommandOptionAttribute(string name)
+ {
+ Name = name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Attributes/DefaultCommandAttribute.cs b/CliFx/Attributes/DefaultCommandAttribute.cs
new file mode 100644
index 0000000..43195cd
--- /dev/null
+++ b/CliFx/Attributes/DefaultCommandAttribute.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace CliFx.Attributes
+{
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public class DefaultCommandAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs
new file mode 100644
index 0000000..75414d2
--- /dev/null
+++ b/CliFx/CliApplication.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using CliFx.Services;
+
+namespace CliFx
+{
+ public partial class CliApplication : ICliApplication
+ {
+ private readonly ICommandResolver _commandResolver;
+
+ public CliApplication(ICommandResolver commandResolver)
+ {
+ _commandResolver = commandResolver;
+ }
+
+ public CliApplication()
+ : this(GetDefaultCommandResolver(Assembly.GetCallingAssembly()))
+ {
+ }
+
+ public async Task RunAsync(IReadOnlyList commandLineArguments)
+ {
+ // Resolve and execute command
+ var command = _commandResolver.ResolveCommand(commandLineArguments);
+ var exitCode = await command.ExecuteAsync();
+
+ // TODO: print message if error?
+
+ return exitCode.Value;
+ }
+ }
+
+ public partial class CliApplication
+ {
+ private static ICommandResolver GetDefaultCommandResolver(Assembly assembly)
+ {
+ var typeProvider = TypeProvider.FromAssembly(assembly);
+ var commandOptionParser = new CommandOptionParser();
+ var commandOptionConverter = new CommandOptionConverter();
+
+ return new CommandResolver(typeProvider, commandOptionParser, commandOptionConverter);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj
new file mode 100644
index 0000000..028d937
--- /dev/null
+++ b/CliFx/CliFx.csproj
@@ -0,0 +1,8 @@
+
+
+
+ netstandard2.0;net45
+ latest
+
+
+
\ No newline at end of file
diff --git a/CliFx/Command.cs b/CliFx/Command.cs
new file mode 100644
index 0000000..7442e7f
--- /dev/null
+++ b/CliFx/Command.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Threading.Tasks;
+using CliFx.Models;
+
+namespace CliFx
+{
+ public abstract class Command
+ {
+ public virtual ExitCode Execute() => throw new InvalidOperationException(
+ "Can't execute command because its execution method is not defined. " +
+ $"Override Execute or ExecuteAsync on {GetType().Name} in order to make it executable.");
+
+ public virtual Task ExecuteAsync() => Task.FromResult(Execute());
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CommandResolveException.cs b/CliFx/Exceptions/CommandResolveException.cs
new file mode 100644
index 0000000..86684f1
--- /dev/null
+++ b/CliFx/Exceptions/CommandResolveException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace CliFx.Exceptions
+{
+ public class CommandResolveException : Exception
+ {
+ public CommandResolveException()
+ {
+ }
+
+ public CommandResolveException(string message)
+ : base(message)
+ {
+ }
+
+ public CommandResolveException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Extensions.cs b/CliFx/Extensions.cs
new file mode 100644
index 0000000..972387e
--- /dev/null
+++ b/CliFx/Extensions.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace CliFx
+{
+ public static class Extensions
+ {
+ public static Task RunAsync(this ICliApplication application) =>
+ application.RunAsync(Environment.GetCommandLineArgs().Skip(1).ToArray());
+ }
+}
\ No newline at end of file
diff --git a/CliFx/ICliApplication.cs b/CliFx/ICliApplication.cs
new file mode 100644
index 0000000..d5f4b33
--- /dev/null
+++ b/CliFx/ICliApplication.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace CliFx
+{
+ public interface ICliApplication
+ {
+ Task RunAsync(IReadOnlyList commandLineArguments);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Internal/CommandOptionProperty.cs b/CliFx/Internal/CommandOptionProperty.cs
new file mode 100644
index 0000000..3b1dd15
--- /dev/null
+++ b/CliFx/Internal/CommandOptionProperty.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Reflection;
+using CliFx.Attributes;
+
+namespace CliFx.Internal
+{
+ internal partial class CommandOptionProperty
+ {
+ private readonly PropertyInfo _property;
+
+ public Type Type => _property.PropertyType;
+
+ public string Name { 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)
+ {
+ _property = property;
+ Name = name;
+ ShortName = shortName;
+ IsRequired = isRequired;
+ Description = description;
+ }
+
+ public void SetValue(Command command, object value) => _property.SetValue(command, value);
+ }
+
+ internal partial class CommandOptionProperty
+ {
+ public static bool IsValid(PropertyInfo property) => property.IsDefined(typeof(CommandOptionAttribute));
+
+ public static CommandOptionProperty Initialize(PropertyInfo property)
+ {
+ if (!IsValid(property))
+ throw new InvalidOperationException($"[{property.Name}] is not a valid command option property.");
+
+ var attribute = property.GetCustomAttribute();
+
+ return new CommandOptionProperty(property, attribute.Name, attribute.ShortName, attribute.IsRequired,
+ attribute.Description);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Internal/CommandType.cs b/CliFx/Internal/CommandType.cs
new file mode 100644
index 0000000..520fcfa
--- /dev/null
+++ b/CliFx/Internal/CommandType.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using CliFx.Attributes;
+
+namespace CliFx.Internal
+{
+ internal partial class CommandType
+ {
+ private readonly Type _type;
+
+ public string Name { get; }
+
+ public bool IsDefault { get; }
+
+ public CommandType(Type type, string name, bool isDefault)
+ {
+ _type = type;
+ Name = name;
+ IsDefault = isDefault;
+ }
+
+ public IEnumerable GetOptionProperties() => _type.GetProperties()
+ .Where(CommandOptionProperty.IsValid)
+ .Select(CommandOptionProperty.Initialize);
+
+ public Command Activate() => (Command) Activator.CreateInstance(_type);
+ }
+
+ internal partial class CommandType
+ {
+ public static bool IsValid(Type type) =>
+ // Derives from Command
+ type.IsDerivedFrom(typeof(Command)) &&
+ // Marked with DefaultCommandAttribute or CommandAttribute
+ (type.IsDefined(typeof(DefaultCommandAttribute)) || type.IsDefined(typeof(CommandAttribute)));
+
+ public static CommandType Initialize(Type type)
+ {
+ if (!IsValid(type))
+ throw new InvalidOperationException($"[{type.Name}] is not a valid command type.");
+
+ var name = type.GetCustomAttribute()?.Name;
+ var isDefault = type.IsDefined(typeof(DefaultCommandAttribute));
+
+ return new CommandType(type, name, isDefault);
+ }
+
+ public static IEnumerable GetCommandTypes(IEnumerable types) => types.Where(IsValid).Select(Initialize);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs
new file mode 100644
index 0000000..ca9bc2a
--- /dev/null
+++ b/CliFx/Internal/Extensions.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace CliFx.Internal
+{
+ internal static class Extensions
+ {
+ public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
+
+ public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
+ {
+ var index = s.IndexOf(sub, comparison);
+ return index < 0 ? s : s.Substring(0, index);
+ }
+
+ public static string SubstringAfter(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
+ {
+ var index = s.IndexOf(sub, comparison);
+ return index < 0 ? string.Empty : s.Substring(index + sub.Length, s.Length - index - sub.Length);
+ }
+
+ public static TValue GetValueOrDefault(this IReadOnlyDictionary dic, TKey key) =>
+ dic.TryGetValue(key, out var result) ? result : default;
+
+ public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
+ {
+ while (s.StartsWith(sub, comparison))
+ s = s.Substring(sub.Length);
+
+ return s;
+ }
+
+ public static string TrimEnd(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
+ {
+ while (s.EndsWith(sub, comparison))
+ s = s.Substring(0, s.Length - sub.Length);
+
+ return s;
+ }
+
+ public static string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source);
+
+ public static bool IsDerivedFrom(this Type type, Type baseType)
+ {
+ var currentType = type;
+ while (currentType != null)
+ {
+ if (currentType == baseType)
+ return true;
+
+ currentType = currentType.BaseType;
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Models/CommandOptionSet.cs b/CliFx/Models/CommandOptionSet.cs
new file mode 100644
index 0000000..fdd40ce
--- /dev/null
+++ b/CliFx/Models/CommandOptionSet.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using CliFx.Internal;
+
+namespace CliFx.Models
+{
+ public partial class CommandOptionSet
+ {
+ public string CommandName { get; }
+
+ public IReadOnlyDictionary Options { get; }
+
+ public CommandOptionSet(string commandName, IReadOnlyDictionary options)
+ {
+ CommandName = commandName;
+ Options = options;
+ }
+
+ public CommandOptionSet(IReadOnlyDictionary options)
+ : this(null, options)
+ {
+ }
+
+ public CommandOptionSet(string commandName)
+ : this(commandName, new Dictionary())
+ {
+ }
+
+ public override string ToString() => !CommandName.IsNullOrWhiteSpace()
+ ? $"{CommandName} / {Options.Count} option(s)"
+ : $"{Options.Count} option(s)";
+ }
+
+ public partial class CommandOptionSet
+ {
+ public static CommandOptionSet Empty { get; } = new CommandOptionSet(new Dictionary());
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Models/ExitCode.cs b/CliFx/Models/ExitCode.cs
new file mode 100644
index 0000000..d730edb
--- /dev/null
+++ b/CliFx/Models/ExitCode.cs
@@ -0,0 +1,26 @@
+using System.Globalization;
+
+namespace CliFx.Models
+{
+ public partial class ExitCode
+ {
+ public int Value { get; }
+
+ public string Message { get; }
+
+ public bool IsSuccess => Value == 0;
+
+ public ExitCode(int value, string message = null)
+ {
+ Value = value;
+ Message = message;
+ }
+
+ public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public partial class ExitCode
+ {
+ public static ExitCode Success { get; } = new ExitCode(0);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/CommandOptionConverter.cs b/CliFx/Services/CommandOptionConverter.cs
new file mode 100644
index 0000000..7c9a526
--- /dev/null
+++ b/CliFx/Services/CommandOptionConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Globalization;
+using CliFx.Internal;
+
+namespace CliFx.Services
+{
+ public class CommandOptionConverter : ICommandOptionConverter
+ {
+ private readonly IFormatProvider _formatProvider;
+
+ public CommandOptionConverter(IFormatProvider formatProvider)
+ {
+ _formatProvider = formatProvider;
+ }
+
+ public CommandOptionConverter()
+ : this(CultureInfo.InvariantCulture)
+ {
+ }
+
+ public object ConvertOption(string value, Type targetType)
+ {
+ // String or object
+ if (targetType == typeof(string) || targetType == typeof(object))
+ return value;
+
+ // Bool
+ if (targetType == typeof(bool))
+ return value.IsNullOrWhiteSpace() || bool.Parse(value);
+
+ // DateTime
+ if (targetType == typeof(DateTime))
+ return DateTime.Parse(value, _formatProvider);
+
+ // DateTimeOffset
+ if (targetType == typeof(DateTimeOffset))
+ return DateTimeOffset.Parse(value, _formatProvider);
+
+ // TimeSpan
+ if (targetType == typeof(TimeSpan))
+ return TimeSpan.Parse(value, _formatProvider);
+
+ // Enum
+ if (targetType.IsEnum)
+ return Enum.Parse(targetType, value, true);
+
+ // Nullable
+ var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
+ if (nullableUnderlyingType != null)
+ return !value.IsNullOrWhiteSpace() ? ConvertOption(value, nullableUnderlyingType) : null;
+
+ // All other types
+ return Convert.ChangeType(value, targetType, _formatProvider);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/CommandOptionParser.cs b/CliFx/Services/CommandOptionParser.cs
new file mode 100644
index 0000000..ada4964
--- /dev/null
+++ b/CliFx/Services/CommandOptionParser.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using CliFx.Internal;
+using CliFx.Models;
+
+namespace CliFx.Services
+{
+ public class CommandOptionParser : ICommandOptionParser
+ {
+ public CommandOptionSet ParseOptions(IReadOnlyList commandLineArguments)
+ {
+ // Initialize command name placeholder
+ string commandName = null;
+
+ // Initialize options
+ var options = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ // Keep track of the last option's name
+ string optionName = null;
+
+ // Loop through all arguments
+ var isFirstArgument = true;
+ foreach (var commandLineArgument in commandLineArguments)
+ {
+ // Option name
+ if (commandLineArgument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
+ {
+ // Extract option name (skip 2 chars)
+ optionName = commandLineArgument.Substring(2);
+ options[optionName] = null;
+ }
+
+ // Short option name
+ else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase) && commandLineArgument.Length == 2)
+ {
+ // Extract option name (skip 1 char)
+ optionName = commandLineArgument.Substring(1);
+ options[optionName] = null;
+ }
+
+ // Multiple stacked short options
+ else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
+ {
+ optionName = null;
+ foreach (var c in commandLineArgument.Substring(1))
+ {
+ options[c.ToString(CultureInfo.InvariantCulture)] = null;
+ }
+ }
+
+ // Command name
+ else if (isFirstArgument)
+ {
+ commandName = commandLineArgument;
+ }
+
+ // Option value
+ else if (!optionName.IsNullOrWhiteSpace())
+ {
+ // ReSharper disable once AssignNullToNotNullAttribute
+ options[optionName] = commandLineArgument;
+ }
+
+ isFirstArgument = false;
+ }
+
+ return new CommandOptionSet(commandName, options);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/CommandResolver.cs b/CliFx/Services/CommandResolver.cs
new file mode 100644
index 0000000..81305da
--- /dev/null
+++ b/CliFx/Services/CommandResolver.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using CliFx.Attributes;
+using CliFx.Exceptions;
+using CliFx.Internal;
+
+namespace CliFx.Services
+{
+ public class CommandResolver : ICommandResolver
+ {
+ private readonly ITypeProvider _typeProvider;
+ private readonly ICommandOptionParser _commandOptionParser;
+ private readonly ICommandOptionConverter _commandOptionConverter;
+
+ public CommandResolver(ITypeProvider typeProvider,
+ ICommandOptionParser commandOptionParser, ICommandOptionConverter commandOptionConverter)
+ {
+ _typeProvider = typeProvider;
+ _commandOptionParser = commandOptionParser;
+ _commandOptionConverter = commandOptionConverter;
+ }
+
+ private IEnumerable GetCommandTypes() => CommandType.GetCommandTypes(_typeProvider.GetTypes());
+
+ private CommandType GetDefaultCommandType()
+ {
+ // Get command types marked as default
+ var defaultCommandTypes = GetCommandTypes().Where(t => t.IsDefault).ToArray();
+
+ // If there's only one type - return
+ if (defaultCommandTypes.Length == 1)
+ return defaultCommandTypes.Single();
+
+ // If there are multiple - throw
+ if (defaultCommandTypes.Length > 1)
+ {
+ throw new CommandResolveException(
+ "Can't resolve default command because there is more than one command marked as default. " +
+ $"Make sure you apply {nameof(DefaultCommandAttribute)} only to one command.");
+ }
+
+ // If there aren't any - throw
+ throw new CommandResolveException(
+ "Can't resolve default command because there are no commands marked as default. " +
+ $"Apply {nameof(DefaultCommandAttribute)} to the default command.");
+ }
+
+ private CommandType GetCommandType(string name)
+ {
+ // Get command types with given name
+ var matchingCommandTypes =
+ GetCommandTypes().Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray();
+
+ // If there's only one type - return
+ if (matchingCommandTypes.Length == 1)
+ return matchingCommandTypes.Single();
+
+ // If there are multiple - throw
+ if (matchingCommandTypes.Length > 1)
+ {
+ throw new CommandResolveException(
+ $"Can't resolve command because there is more than one command named [{name}]. " +
+ "Make sure all command names are unique and keep in mind that comparison is case-insensitive.");
+ }
+
+ // If there aren't any - throw
+ throw new CommandResolveException(
+ $"Can't resolve command because none of the commands is named [{name}]. " +
+ $"Apply {nameof(CommandAttribute)} to give command a name.");
+ }
+
+ public Command ResolveCommand(IReadOnlyList commandLineArguments)
+ {
+ var optionSet = _commandOptionParser.ParseOptions(commandLineArguments);
+
+ // Get command type
+ var commandType = !optionSet.CommandName.IsNullOrWhiteSpace()
+ ? GetCommandType(optionSet.CommandName)
+ : GetDefaultCommandType();
+
+ // Activate command
+ var command = commandType.Activate();
+
+ // 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))
+ {
+ var convertedValue = _commandOptionConverter.ConvertOption(value, 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.");
+ }
+ }
+
+ return command;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs
new file mode 100644
index 0000000..c391a19
--- /dev/null
+++ b/CliFx/Services/Extensions.cs
@@ -0,0 +1,7 @@
+namespace CliFx.Services
+{
+ public static class Extensions
+ {
+ public static Command ResolveCommand(this ICommandResolver commandResolver) => commandResolver.ResolveCommand(new string[0]);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandOptionConverter.cs b/CliFx/Services/ICommandOptionConverter.cs
new file mode 100644
index 0000000..395bf30
--- /dev/null
+++ b/CliFx/Services/ICommandOptionConverter.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace CliFx.Services
+{
+ public interface ICommandOptionConverter
+ {
+ object ConvertOption(string value, Type targetType);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandOptionParser.cs b/CliFx/Services/ICommandOptionParser.cs
new file mode 100644
index 0000000..7cd4c1f
--- /dev/null
+++ b/CliFx/Services/ICommandOptionParser.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using CliFx.Models;
+
+namespace CliFx.Services
+{
+ public interface ICommandOptionParser
+ {
+ CommandOptionSet ParseOptions(IReadOnlyList commandLineArguments);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandResolver.cs b/CliFx/Services/ICommandResolver.cs
new file mode 100644
index 0000000..76e8407
--- /dev/null
+++ b/CliFx/Services/ICommandResolver.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace CliFx.Services
+{
+ public interface ICommandResolver
+ {
+ Command ResolveCommand(IReadOnlyList commandLineArguments);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ITypeProvider.cs b/CliFx/Services/ITypeProvider.cs
new file mode 100644
index 0000000..bc7c5da
--- /dev/null
+++ b/CliFx/Services/ITypeProvider.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+
+namespace CliFx.Services
+{
+ public interface ITypeProvider
+ {
+ IReadOnlyList GetTypes();
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/TypeProvider.cs b/CliFx/Services/TypeProvider.cs
new file mode 100644
index 0000000..93c92f7
--- /dev/null
+++ b/CliFx/Services/TypeProvider.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace CliFx.Services
+{
+ public partial class TypeProvider : ITypeProvider
+ {
+ private readonly IReadOnlyList _types;
+
+ public TypeProvider(IReadOnlyList types)
+ {
+ _types = types;
+ }
+
+ public TypeProvider(params Type[] types)
+ : this((IReadOnlyList) types)
+ {
+ }
+
+ public IReadOnlyList GetTypes() => _types;
+ }
+
+ public partial class TypeProvider
+ {
+ public static TypeProvider FromAssembly(Assembly assembly) => new TypeProvider(assembly.GetExportedTypes());
+
+ public static TypeProvider FromAssemblies(IReadOnlyList assemblies) =>
+ new TypeProvider(assemblies.SelectMany(a => a.ExportedTypes).ToArray());
+
+ public static TypeProvider FromAssemblies(params Assembly[] assemblies) => FromAssemblies((IReadOnlyList) assemblies);
+ }
+}
\ No newline at end of file