mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Add project files.
This commit is contained in:
13
CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
Normal file
13
CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
25
CliFx.Tests.Dummy/Commands/AddCommand.cs
Normal file
25
CliFx.Tests.Dummy/Commands/AddCommand.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
CliFx.Tests.Dummy/Commands/DefaultCommand.cs
Normal file
31
CliFx.Tests.Dummy/Commands/DefaultCommand.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
CliFx.Tests.Dummy/Commands/LogCommand.cs
Normal file
25
CliFx.Tests.Dummy/Commands/LogCommand.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests.Dummy/Program.cs
Normal file
9
CliFx.Tests.Dummy/Program.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Dummy
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static Task<int> Main(string[] args) => new CliApplication().RunAsync(args);
|
||||
}
|
||||
}
|
||||
33
CliFx.Tests/CliApplicationTests.cs
Normal file
33
CliFx.Tests/CliApplicationTests.cs
Normal file
@@ -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<ICommandResolver>();
|
||||
commandResolverMock.Setup(m => m.ResolveCommand(It.IsAny<IReadOnlyList<string>>())).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));
|
||||
}
|
||||
}
|
||||
}
|
||||
23
CliFx.Tests/CliFx.Tests.csproj
Normal file
23
CliFx.Tests/CliFx.Tests.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
|
||||
<PackageReference Include="Moq" Version="4.11.0" />
|
||||
<PackageReference Include="CliWrap" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" />
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
83
CliFx.Tests/CommandOptionConverterTests.cs
Normal file
83
CliFx.Tests/CommandOptionConverterTests.cs
Normal file
@@ -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<TestCaseData> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
139
CliFx.Tests/CommandOptionParserTests.cs
Normal file
139
CliFx.Tests/CommandOptionParserTests.cs
Normal file
@@ -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<TestCaseData> 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<string, string>
|
||||
{
|
||||
{"argument", "value"}
|
||||
})
|
||||
).SetName("Single argument");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--argument1", "value1", "--argument2", "value2", "--argument3", "value3"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"argument1", "value1"},
|
||||
{"argument2", "value2"},
|
||||
{"argument3", "value3"}
|
||||
})
|
||||
).SetName("Multiple arguments");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"a", "value"}
|
||||
})
|
||||
).SetName("Single short argument");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "-b", "value2", "-c", "value3"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"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<string, string>
|
||||
{
|
||||
{"argument1", "value1"},
|
||||
{"b", "value2"},
|
||||
{"argument3", "value3"}
|
||||
})
|
||||
).SetName("Multiple mixed arguments");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"switch", null}
|
||||
})
|
||||
).SetName("Single switch");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch1", "--switch2", "--switch3"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"switch1", null},
|
||||
{"switch2", null},
|
||||
{"switch3", null}
|
||||
})
|
||||
).SetName("Multiple switches");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-s"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"s", null}
|
||||
})
|
||||
).SetName("Single short switch");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "-b", "-c"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"a", null},
|
||||
{"b", null},
|
||||
{"c", null}
|
||||
})
|
||||
).SetName("Multiple short switches");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-abc"},
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"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<string, string>
|
||||
{
|
||||
{"argument", "value"}
|
||||
})
|
||||
).SetName("Single argument (with command name)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetData_ParseOptions))]
|
||||
public void ParseOptions_Test(IReadOnlyList<string> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
116
CliFx.Tests/CommandResolverTests.cs
Normal file
116
CliFx.Tests/CommandResolverTests.cs
Normal file
@@ -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<TestCaseData> GetData_ResolveCommand()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"int", "13"}
|
||||
}),
|
||||
new TestCommand {IntOption = 13}
|
||||
).SetName("Single option");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"int", "13"},
|
||||
{"str", "hello world" }
|
||||
}),
|
||||
new TestCommand { IntOption = 13, StringOption = "hello world"}
|
||||
).SetName("Multiple options");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"i", "13"}
|
||||
}),
|
||||
new TestCommand { IntOption = 13 }
|
||||
).SetName("Single short option");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new CommandOptionSet("command", new Dictionary<string, string>
|
||||
{
|
||||
{"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<ITypeProvider>();
|
||||
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
|
||||
var typeProvider = typeProviderMock.Object;
|
||||
|
||||
var optionParserMock = new Mock<ICommandOptionParser>();
|
||||
optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).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<TestCaseData> GetData_ResolveCommand_IsRequired()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
CommandOptionSet.Empty
|
||||
).SetName("No options");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new CommandOptionSet(new Dictionary<string, string>
|
||||
{
|
||||
{"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<ITypeProvider>();
|
||||
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
|
||||
var typeProvider = typeProviderMock.Object;
|
||||
|
||||
var optionParserMock = new Mock<ICommandOptionParser>();
|
||||
optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).Returns(commandOptionSet);
|
||||
var optionParser = optionParserMock.Object;
|
||||
|
||||
var optionConverter = new CommandOptionConverter();
|
||||
|
||||
var resolver = new CommandResolver(typeProvider, optionParser, optionConverter);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<CommandResolveException>(() => resolver.ResolveCommand());
|
||||
}
|
||||
}
|
||||
}
|
||||
32
CliFx.Tests/DummyTests.cs
Normal file
32
CliFx.Tests/DummyTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/TestObjects/TestCommand.cs
Normal file
18
CliFx.Tests/TestObjects/TestCommand.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests/TestObjects/TestEnum.cs
Normal file
9
CliFx.Tests/TestObjects/TestEnum.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CliFx.Tests.TestObjects
|
||||
{
|
||||
public enum TestEnum
|
||||
{
|
||||
Value1,
|
||||
Value2,
|
||||
Value3
|
||||
}
|
||||
}
|
||||
65
CliFx.sln
Normal file
65
CliFx.sln
Normal file
@@ -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
|
||||
15
CliFx/Attributes/CommandAttribute.cs
Normal file
15
CliFx/Attributes/CommandAttribute.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CliFx/Attributes/CommandOptionAttribute.cs
Normal file
21
CliFx/Attributes/CommandOptionAttribute.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
CliFx/Attributes/DefaultCommandAttribute.cs
Normal file
9
CliFx/Attributes/DefaultCommandAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class DefaultCommandAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
45
CliFx/CliApplication.cs
Normal file
45
CliFx/CliApplication.cs
Normal file
@@ -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<int> RunAsync(IReadOnlyList<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
CliFx/CliFx.csproj
Normal file
8
CliFx/CliFx.csproj
Normal file
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
15
CliFx/Command.cs
Normal file
15
CliFx/Command.cs
Normal file
@@ -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<ExitCode> ExecuteAsync() => Task.FromResult(Execute());
|
||||
}
|
||||
}
|
||||
21
CliFx/Exceptions/CommandResolveException.cs
Normal file
21
CliFx/Exceptions/CommandResolveException.cs
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
12
CliFx/Extensions.cs
Normal file
12
CliFx/Extensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<int> RunAsync(this ICliApplication application) =>
|
||||
application.RunAsync(Environment.GetCommandLineArgs().Skip(1).ToArray());
|
||||
}
|
||||
}
|
||||
10
CliFx/ICliApplication.cs
Normal file
10
CliFx/ICliApplication.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public interface ICliApplication
|
||||
{
|
||||
Task<int> RunAsync(IReadOnlyList<string> commandLineArguments);
|
||||
}
|
||||
}
|
||||
48
CliFx/Internal/CommandOptionProperty.cs
Normal file
48
CliFx/Internal/CommandOptionProperty.cs
Normal file
@@ -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<CommandOptionAttribute>();
|
||||
|
||||
return new CommandOptionProperty(property, attribute.Name, attribute.ShortName, attribute.IsRequired,
|
||||
attribute.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
CliFx/Internal/CommandType.cs
Normal file
52
CliFx/Internal/CommandType.cs
Normal file
@@ -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<CommandOptionProperty> 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<CommandAttribute>()?.Name;
|
||||
var isDefault = type.IsDefined(typeof(DefaultCommandAttribute));
|
||||
|
||||
return new CommandType(type, name, isDefault);
|
||||
}
|
||||
|
||||
public static IEnumerable<CommandType> GetCommandTypes(IEnumerable<Type> types) => types.Where(IsValid).Select(Initialize);
|
||||
}
|
||||
}
|
||||
57
CliFx/Internal/Extensions.cs
Normal file
57
CliFx/Internal/Extensions.cs
Normal file
@@ -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<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> 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<T>(this IEnumerable<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
CliFx/Models/CommandOptionSet.cs
Normal file
37
CliFx/Models/CommandOptionSet.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public partial class CommandOptionSet
|
||||
{
|
||||
public string CommandName { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Options { get; }
|
||||
|
||||
public CommandOptionSet(string commandName, IReadOnlyDictionary<string, string> options)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public CommandOptionSet(IReadOnlyDictionary<string, string> options)
|
||||
: this(null, options)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandOptionSet(string commandName)
|
||||
: this(commandName, new Dictionary<string, string>())
|
||||
{
|
||||
}
|
||||
|
||||
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<string, string>());
|
||||
}
|
||||
}
|
||||
26
CliFx/Models/ExitCode.cs
Normal file
26
CliFx/Models/ExitCode.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
56
CliFx/Services/CommandOptionConverter.cs
Normal file
56
CliFx/Services/CommandOptionConverter.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
CliFx/Services/CommandOptionParser.cs
Normal file
71
CliFx/Services/CommandOptionParser.cs
Normal file
@@ -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<string> commandLineArguments)
|
||||
{
|
||||
// Initialize command name placeholder
|
||||
string commandName = null;
|
||||
|
||||
// Initialize options
|
||||
var options = new Dictionary<string, string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
CliFx/Services/CommandResolver.cs
Normal file
107
CliFx/Services/CommandResolver.cs
Normal file
@@ -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<CommandType> 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<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
CliFx/Services/Extensions.cs
Normal file
7
CliFx/Services/Extensions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Command ResolveCommand(this ICommandResolver commandResolver) => commandResolver.ResolveCommand(new string[0]);
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/ICommandOptionConverter.cs
Normal file
9
CliFx/Services/ICommandOptionConverter.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandOptionConverter
|
||||
{
|
||||
object ConvertOption(string value, Type targetType);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandOptionParser.cs
Normal file
10
CliFx/Services/ICommandOptionParser.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandOptionParser
|
||||
{
|
||||
CommandOptionSet ParseOptions(IReadOnlyList<string> commandLineArguments);
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/ICommandResolver.cs
Normal file
9
CliFx/Services/ICommandResolver.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandResolver
|
||||
{
|
||||
Command ResolveCommand(IReadOnlyList<string> commandLineArguments);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ITypeProvider.cs
Normal file
10
CliFx/Services/ITypeProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ITypeProvider
|
||||
{
|
||||
IReadOnlyList<Type> GetTypes();
|
||||
}
|
||||
}
|
||||
34
CliFx/Services/TypeProvider.cs
Normal file
34
CliFx/Services/TypeProvider.cs
Normal file
@@ -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<Type> _types;
|
||||
|
||||
public TypeProvider(IReadOnlyList<Type> types)
|
||||
{
|
||||
_types = types;
|
||||
}
|
||||
|
||||
public TypeProvider(params Type[] types)
|
||||
: this((IReadOnlyList<Type>) types)
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<Type> GetTypes() => _types;
|
||||
}
|
||||
|
||||
public partial class TypeProvider
|
||||
{
|
||||
public static TypeProvider FromAssembly(Assembly assembly) => new TypeProvider(assembly.GetExportedTypes());
|
||||
|
||||
public static TypeProvider FromAssemblies(IReadOnlyList<Assembly> assemblies) =>
|
||||
new TypeProvider(assemblies.SelectMany(a => a.ExportedTypes).ToArray());
|
||||
|
||||
public static TypeProvider FromAssemblies(params Assembly[] assemblies) => FromAssemblies((IReadOnlyList<Assembly>) assemblies);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user