mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Rework architecture again
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<TargetFramework>net46</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>1.2.3.4</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command]
|
||||
public class DefaultCommand : Command
|
||||
{
|
||||
[CommandOption("target", 't', Description = "Greeting target.")]
|
||||
public string Target { get; set; } = "world";
|
||||
|
||||
[CommandOption('e', Description = "Whether the greeting should be enthusiastic.")]
|
||||
public bool IsEnthusiastic { get; set; }
|
||||
|
||||
protected override ExitCode Process()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer.Append("Hello ").Append(Target);
|
||||
|
||||
if (IsEnthusiastic)
|
||||
buffer.Append("!!!");
|
||||
|
||||
Output.WriteLine(buffer.ToString());
|
||||
|
||||
return ExitCode.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
CliFx.Tests.Dummy/Commands/GreeterCommand.cs
Normal file
32
CliFx.Tests.Dummy/Commands/GreeterCommand.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command]
|
||||
public class GreeterCommand : ICommand
|
||||
{
|
||||
[CommandOption("target", 't', Description = "Greeting target.")]
|
||||
public string Target { get; set; } = "world";
|
||||
|
||||
[CommandOption('e', Description = "Whether the greeting should be exclaimed.")]
|
||||
public bool IsExclaimed { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer.Append("Hello").Append(' ').Append(Target);
|
||||
|
||||
if (IsExclaimed)
|
||||
buffer.Append('!');
|
||||
|
||||
context.Output.WriteLine(buffer.ToString());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command("log", Description = "Calculate the logarithm of a value.")]
|
||||
public class LogCommand : Command
|
||||
[Command("log", Description = "Calculates the logarithm of a value.")]
|
||||
public class LogCommand : ICommand
|
||||
{
|
||||
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
||||
public double Value { get; set; }
|
||||
@@ -15,12 +15,12 @@ namespace CliFx.Tests.Dummy.Commands
|
||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||
public double Base { get; set; } = 10;
|
||||
|
||||
protected override ExitCode Process()
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
{
|
||||
var result = Math.Log(Value, Base);
|
||||
Output.WriteLine(result.ToString(CultureInfo.InvariantCulture));
|
||||
context.Output.WriteLine(result);
|
||||
|
||||
return ExitCode.Success;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command("add", Description = "Calculate the sum of all input values.")]
|
||||
public class AddCommand : Command
|
||||
[Command("sum", Description = "Calculates the sum of all input values.")]
|
||||
public class SumCommand : ICommand
|
||||
{
|
||||
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
|
||||
public IReadOnlyList<double> Values { get; set; }
|
||||
|
||||
protected override ExitCode Process()
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
{
|
||||
var result = Values.Sum();
|
||||
Output.WriteLine(result.ToString(CultureInfo.InvariantCulture));
|
||||
context.Output.WriteLine(result);
|
||||
|
||||
return ExitCode.Success;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
@@ -9,32 +11,187 @@ namespace CliFx.Tests
|
||||
public partial class CliApplicationTests
|
||||
{
|
||||
[Command]
|
||||
public class TestCommand : ICommand
|
||||
private class TestDefaultCommand : ICommand
|
||||
{
|
||||
public static ExitCode ExitCode { get; } = new ExitCode(13);
|
||||
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
public CommandContext Context { get; set; }
|
||||
[Command("command")]
|
||||
private class TestNamedCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<ExitCode> ExecuteAsync() => Task.FromResult(ExitCode);
|
||||
[Command("faulty-command")]
|
||||
private class FaultyCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => Task.FromException(new CommandErrorException(-1337));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public partial class CliApplicationTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
||||
{
|
||||
// Specified command is defined
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestNamedCommand)},
|
||||
new[] {"command"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestNamedCommand)},
|
||||
new[] {"command", "--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestNamedCommand)},
|
||||
new[] {"command", "-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestNamedCommand)},
|
||||
new[] {"command", "-?"}
|
||||
);
|
||||
|
||||
|
||||
// Default command is defined
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestDefaultCommand)},
|
||||
new string[0]
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestDefaultCommand)},
|
||||
new[] {"--version"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestDefaultCommand)},
|
||||
new[] {"--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestDefaultCommand)},
|
||||
new[] {"-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestDefaultCommand)},
|
||||
new[] {"-?"}
|
||||
);
|
||||
|
||||
// Default command is not defined
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new string[0]
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"--version"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"-?"}
|
||||
);
|
||||
|
||||
// Specified a faulty command
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand)},
|
||||
new[] {"--version"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand)},
|
||||
new[] {"--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand)},
|
||||
new[] {"-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand)},
|
||||
new[] {"-?"}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
|
||||
{
|
||||
// Specified command is not defined
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"command"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"command", "--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"command", "-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new[] {"command", "-?"}
|
||||
);
|
||||
|
||||
// Specified a faulty command
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand)},
|
||||
new[] {"faulty-command"}
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_Test()
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
// Arrange
|
||||
var application = new CliApplication(
|
||||
new CommandInputParser(),
|
||||
new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})));
|
||||
var application = new CliApplication(commandTypes);
|
||||
|
||||
// Act
|
||||
var exitCodeValue = await application.RunAsync();
|
||||
var exitCodeValue = await application.RunAsync(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
Assert.That(exitCodeValue, Is.EqualTo(TestCommand.ExitCode.Value), "Exit code");
|
||||
Assert.That(exitCodeValue, Is.Zero, "Exit code");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
|
||||
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
// Arrange
|
||||
var application = new CliApplication(commandTypes);
|
||||
|
||||
// Act
|
||||
var exitCodeValue = await application.RunAsync(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
Assert.That(exitCodeValue, Is.Not.Zero, "Exit code");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<TargetFramework>net46</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
|
||||
41
CliFx.Tests/CommandFactoryTests.cs
Normal file
41
CliFx.Tests/CommandFactoryTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandFactoryTests
|
||||
{
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public partial class CommandFactoryTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||
{
|
||||
yield return new TestCaseData(typeof(TestCommand));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
|
||||
public void CreateCommand_Test(Type commandType)
|
||||
{
|
||||
// Arrange
|
||||
var factory = new CommandFactory();
|
||||
|
||||
// Act
|
||||
var schema = new CommandSchemaResolver().GetCommandSchema(commandType);
|
||||
var command = factory.CreateCommand(schema);
|
||||
|
||||
// Assert
|
||||
Assert.That(command, Is.TypeOf(commandType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
@@ -10,7 +11,6 @@ namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandInitializerTests
|
||||
{
|
||||
[Command]
|
||||
public class TestCommand : ICommand
|
||||
{
|
||||
[CommandOption("int", 'i', IsRequired = true)]
|
||||
@@ -22,9 +22,7 @@ namespace CliFx.Tests
|
||||
[CommandOption("bool", 'b', GroupName = "other-group")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public CommandContext Context { get; set; }
|
||||
|
||||
public Task<ExitCode> ExecuteAsync() => throw new System.NotImplementedException();
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,26 +92,6 @@ namespace CliFx.Tests
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
||||
public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
|
||||
|
||||
// Act
|
||||
var command = initializer.InitializeCommand(commandInput) as TestCommand;
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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));
|
||||
Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_IsRequired()
|
||||
{
|
||||
yield return new TestCaseData(CommandInput.Empty);
|
||||
@@ -126,15 +104,38 @@ namespace CliFx.Tests
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
||||
public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act
|
||||
var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
|
||||
var command = new TestCommand();
|
||||
initializer.InitializeCommand(command, schema, commandInput);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
|
||||
Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
|
||||
Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))]
|
||||
public void InitializeCommand_IsRequired_Test(CommandInput commandInput)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<CommandResolveException>(() => initializer.InitializeCommand(commandInput));
|
||||
var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
|
||||
var command = new TestCommand();
|
||||
Assert.Throws<MissingCommandOptionException>(() => initializer.InitializeCommand(command, schema, commandInput));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
@@ -10,8 +11,10 @@ namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandSchemaResolverTests
|
||||
{
|
||||
[Command(Description = "Command description")]
|
||||
public class TestCommand : ICommand
|
||||
[Command("Command name", Description = "Command description")]
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', GroupName = "Group 1")]
|
||||
public int OptionA { get; set; }
|
||||
@@ -22,9 +25,7 @@ namespace CliFx.Tests
|
||||
[CommandOption("option-c", Description = "Option C description")]
|
||||
public bool OptionC { get; set; }
|
||||
|
||||
public CommandContext Context { get; set; }
|
||||
|
||||
public Task<ExitCode> ExecuteAsync() => throw new NotImplementedException();
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,36 +35,32 @@ namespace CliFx.Tests
|
||||
private static IEnumerable<TestCaseData> GetTestCases_ResolveAllSchemas()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(TestCommand)},
|
||||
new[]
|
||||
{
|
||||
new CommandSchema(typeof(TestCommand),
|
||||
null, true, "Command description",
|
||||
typeof(TestCommand),
|
||||
new CommandSchema(typeof(TestCommand), "Command name", "Command description",
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)),
|
||||
"option-a", 'a', false, "Group 1", null),
|
||||
"option-a", 'a', "Group 1", false, null),
|
||||
new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionB)),
|
||||
"option-b", null, true, null, null),
|
||||
"option-b", null, null, true, null),
|
||||
new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionC)),
|
||||
"option-c", null, false, null, "Option C description")
|
||||
"option-c", null, null, false, "Option C description")
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))]
|
||||
public void ResolveAllSchemas_Test(IReadOnlyList<Type> sourceTypes, IReadOnlyList<CommandSchema> expectedSchemas)
|
||||
public void GetCommandSchema_Test(Type commandType, CommandSchema expectedSchema)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new CommandSchemaResolver(sourceTypes);
|
||||
var resolver = new CommandSchemaResolver();
|
||||
|
||||
// Act
|
||||
var schemas = resolver.ResolveAllSchemas();
|
||||
var schema = resolver.GetCommandSchema(commandType);
|
||||
|
||||
// Assert
|
||||
Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance));
|
||||
Assert.That(schema, Is.EqualTo(expectedSchema).Using(CommandSchemaEqualityComparer.Instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,61 +14,59 @@ namespace CliFx.Tests
|
||||
[Test]
|
||||
[TestCase("", "Hello world")]
|
||||
[TestCase("-t .NET", "Hello .NET")]
|
||||
[TestCase("-e", "Hello world!!!")]
|
||||
[TestCase("add -v 1 2", "3")]
|
||||
[TestCase("add -v 2.75 3.6 4.18", "10.53")]
|
||||
[TestCase("add -v 4 -v 16", "20")]
|
||||
[TestCase("-e", "Hello world!")]
|
||||
[TestCase("sum -v 1 2", "3")]
|
||||
[TestCase("sum -v 2.75 3.6 4.18", "10.53")]
|
||||
[TestCase("sum -v 4 -v 16", "20")]
|
||||
[TestCase("sum --values 2 5 --values 3", "10")]
|
||||
[TestCase("log -v 100", "2")]
|
||||
[TestCase("log --value 256 --base 2", "8")]
|
||||
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
|
||||
{
|
||||
// Act
|
||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
||||
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
|
||||
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("--version")]
|
||||
public async Task CliApplication_RunAsync_Version_Test(string arguments)
|
||||
public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments)
|
||||
{
|
||||
// Act
|
||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
||||
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout");
|
||||
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("--help")]
|
||||
[TestCase("-h")]
|
||||
[TestCase("add -h")]
|
||||
[TestCase("add --help")]
|
||||
[TestCase("sum -h")]
|
||||
[TestCase("sum --help")]
|
||||
[TestCase("log -h")]
|
||||
[TestCase("log --help")]
|
||||
public async Task CliApplication_RunAsync_Help_Test(string arguments)
|
||||
public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments)
|
||||
{
|
||||
// Act
|
||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
||||
Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout");
|
||||
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ namespace CliFx.Attributes
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsDefault => Name.IsNullOrWhiteSpace();
|
||||
|
||||
public CommandAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
|
||||
@@ -9,10 +9,10 @@ namespace CliFx.Attributes
|
||||
|
||||
public char? ShortName { get; }
|
||||
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
public string GroupName { get; set; }
|
||||
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public CommandOptionAttribute(string name, char? shortName)
|
||||
|
||||
@@ -1,33 +1,137 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public class CliApplication : ICliApplication
|
||||
public partial class CliApplication : ICliApplication
|
||||
{
|
||||
private readonly IReadOnlyList<Type> _commandTypes;
|
||||
private readonly ICommandInputParser _commandInputParser;
|
||||
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
||||
private readonly ICommandFactory _commandFactory;
|
||||
private readonly ICommandInitializer _commandInitializer;
|
||||
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
|
||||
|
||||
public CliApplication(ICommandInputParser commandInputParser, ICommandInitializer commandInitializer)
|
||||
public CliApplication(IReadOnlyList<Type> commandTypes,
|
||||
ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
|
||||
ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextBuilder commandHelpTextBuilder)
|
||||
{
|
||||
_commandTypes = commandTypes;
|
||||
_commandInputParser = commandInputParser;
|
||||
_commandSchemaResolver = commandSchemaResolver;
|
||||
_commandFactory = commandFactory;
|
||||
_commandInitializer = commandInitializer;
|
||||
_commandHelpTextBuilder = commandHelpTextBuilder;
|
||||
}
|
||||
|
||||
public CliApplication(IReadOnlyList<Type> commandTypes)
|
||||
: this(commandTypes,
|
||||
new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(),
|
||||
new CommandInitializer(), new CommandHelpTextBuilder())
|
||||
{
|
||||
}
|
||||
|
||||
public CliApplication()
|
||||
: this(new CommandInputParser(), new CommandInitializer())
|
||||
: this(GetDefaultCommandTypes())
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
var input = _commandInputParser.ParseInput(commandLineArguments);
|
||||
var command = _commandInitializer.InitializeCommand(input);
|
||||
var stdOut = ConsoleWriter.GetStandardOutput();
|
||||
var stdErr = ConsoleWriter.GetStandardError();
|
||||
|
||||
var exitCode = await command.ExecuteAsync();
|
||||
try
|
||||
{
|
||||
var commandInput = _commandInputParser.ParseInput(commandLineArguments);
|
||||
|
||||
return exitCode.Value;
|
||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_commandTypes);
|
||||
var matchingCommandSchema = availableCommandSchemas.FindByNameOrNull(commandInput.CommandName);
|
||||
|
||||
// Fail if specified a command which is not defined
|
||||
if (commandInput.IsCommandSpecified() && matchingCommandSchema == null)
|
||||
{
|
||||
stdErr.WriteLine($"Specified command [{commandInput.CommandName}] doesn't exist.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Show version if it was requested without specifying a command
|
||||
if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
|
||||
{
|
||||
var versionText = Assembly.GetEntryAssembly()?.GetName().Version.ToString();
|
||||
stdOut.WriteLine(versionText);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use a stub if command was not specified but there is no default command defined
|
||||
if (matchingCommandSchema == null)
|
||||
{
|
||||
matchingCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand));
|
||||
}
|
||||
|
||||
// Show help if it was requested
|
||||
if (commandInput.IsHelpRequested())
|
||||
{
|
||||
var helpText = _commandHelpTextBuilder.Build(availableCommandSchemas, matchingCommandSchema);
|
||||
stdOut.WriteLine(helpText);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create an instance of the command
|
||||
var command = matchingCommandSchema.Type == typeof(StubDefaultCommand)
|
||||
? new StubDefaultCommand(_commandHelpTextBuilder)
|
||||
: _commandFactory.CreateCommand(matchingCommandSchema);
|
||||
|
||||
// Populate command with options according to its schema
|
||||
_commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput);
|
||||
|
||||
// Create context and execute command
|
||||
var commandContext = new CommandContext(commandInput, availableCommandSchemas, matchingCommandSchema, stdOut, stdErr);
|
||||
await command.ExecuteAsync(commandContext);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stdErr.WriteLine(ex.ToString());
|
||||
return ex is CommandErrorException errorException ? errorException.ExitCode : -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stdOut.Dispose();
|
||||
stdErr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CliApplication
|
||||
{
|
||||
private static IReadOnlyList<Type> GetDefaultCommandTypes() =>
|
||||
Assembly.GetEntryAssembly()?.ExportedTypes.Where(t => t.Implements(typeof(ICommand))).ToArray() ??
|
||||
Type.EmptyTypes;
|
||||
|
||||
private sealed class StubDefaultCommand : ICommand
|
||||
{
|
||||
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
|
||||
|
||||
public StubDefaultCommand(ICommandHelpTextBuilder commandHelpTextBuilder)
|
||||
{
|
||||
_commandHelpTextBuilder = commandHelpTextBuilder;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
{
|
||||
var helpText = _commandHelpTextBuilder.Build(context.AvailableCommandSchemas, context.MatchingCommandSchema);
|
||||
context.Output.WriteLine(helpText);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
||||
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.0.1</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public abstract class Command : ICommand
|
||||
{
|
||||
[CommandOption("help", 'h', GroupName = "__help", Description = "Shows help.")]
|
||||
public bool IsHelpRequested { get; set; }
|
||||
|
||||
[CommandOption("version", GroupName = "__version", Description = "Shows application version.")]
|
||||
public bool IsVersionRequested { get; set; }
|
||||
|
||||
public CommandContext Context { get; set; }
|
||||
|
||||
public IConsoleWriter Output { get; set; } = ConsoleWriter.GetStandardOutput();
|
||||
|
||||
public IConsoleWriter Error { get; set; } = ConsoleWriter.GetStandardError();
|
||||
|
||||
protected virtual ExitCode Process() => throw new InvalidOperationException(
|
||||
"Can't execute command because its execution method is not defined. " +
|
||||
$"Override {nameof(Process)} or {nameof(ProcessAsync)} on {GetType().Name} in order to make it executable.");
|
||||
|
||||
protected virtual Task<ExitCode> ProcessAsync() => Task.FromResult(Process());
|
||||
|
||||
protected virtual void ShowHelp()
|
||||
{
|
||||
var text = new HelpTextBuilder().Build(Context);
|
||||
Output.WriteLine(text);
|
||||
}
|
||||
|
||||
protected virtual void ShowVersion()
|
||||
{
|
||||
var text = Assembly.GetEntryAssembly()?.GetName().Version.ToString();
|
||||
Output.WriteLine(text);
|
||||
}
|
||||
|
||||
public Task<ExitCode> ExecuteAsync()
|
||||
{
|
||||
if (IsHelpRequested)
|
||||
{
|
||||
ShowHelp();
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
if (IsVersionRequested && Context.CommandSchema.IsDefault)
|
||||
{
|
||||
ShowVersion();
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
return ProcessAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CliFx/Exceptions/CannotConvertCommandOptionException.cs
Normal file
21
CliFx/Exceptions/CannotConvertCommandOptionException.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
public class CannotConvertCommandOptionException : Exception
|
||||
{
|
||||
public CannotConvertCommandOptionException()
|
||||
{
|
||||
}
|
||||
|
||||
public CannotConvertCommandOptionException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public CannotConvertCommandOptionException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
30
CliFx/Exceptions/CommandErrorException.cs
Normal file
30
CliFx/Exceptions/CommandErrorException.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
public class CommandErrorException : Exception
|
||||
{
|
||||
public int ExitCode { get; }
|
||||
|
||||
public CommandErrorException(int exitCode, string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
public CommandErrorException(int exitCode, Exception innerException)
|
||||
: this(exitCode, null, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandErrorException(int exitCode, string message)
|
||||
: this(exitCode, message, null)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandErrorException(int exitCode)
|
||||
: this(exitCode, null, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
public class CommandOptionConvertException : Exception
|
||||
{
|
||||
public CommandOptionConvertException()
|
||||
{
|
||||
}
|
||||
|
||||
public CommandOptionConvertException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandOptionConvertException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CliFx/Exceptions/MissingCommandOptionException.cs
Normal file
21
CliFx/Exceptions/MissingCommandOptionException.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
public class MissingCommandOptionException : Exception
|
||||
{
|
||||
public MissingCommandOptionException()
|
||||
{
|
||||
}
|
||||
|
||||
public MissingCommandOptionException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public MissingCommandOptionException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<int> RunAsync(this ICliApplication application) => application.RunAsync(new string[0]);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ namespace CliFx
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
CommandContext Context { get; set; }
|
||||
|
||||
Task<ExitCode> ExecuteAsync();
|
||||
Task ExecuteAsync(CommandContext context);
|
||||
}
|
||||
}
|
||||
@@ -11,49 +11,12 @@ namespace CliFx.Internal
|
||||
|
||||
public static string AsString(this char c) => new string(c, 1);
|
||||
|
||||
public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
||||
{
|
||||
var index = s.IndexOf(sub, comparison);
|
||||
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 string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
|
||||
|
||||
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)
|
||||
{
|
||||
for (var currentType = type; currentType != null; currentType = currentType.BaseType)
|
||||
{
|
||||
if (currentType == baseType)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
|
||||
|
||||
public static bool IsEnumerable(this Type type) =>
|
||||
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
|
||||
@@ -79,5 +42,7 @@ namespace CliFx.Internal
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public class CommandContext
|
||||
{
|
||||
public CommandInput CommandInput { get; }
|
||||
|
||||
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
|
||||
|
||||
public CommandSchema CommandSchema { get; }
|
||||
public CommandSchema MatchingCommandSchema { get; }
|
||||
|
||||
public CommandContext(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema commandSchema)
|
||||
public IConsoleWriter Output { get; }
|
||||
|
||||
public IConsoleWriter Error { get; }
|
||||
|
||||
public CommandContext(CommandInput commandInput,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema,
|
||||
IConsoleWriter output, IConsoleWriter error)
|
||||
{
|
||||
CommandInput = commandInput;
|
||||
AvailableCommandSchemas = availableCommandSchemas;
|
||||
CommandSchema = commandSchema;
|
||||
MatchingCommandSchema = matchingCommandSchema;
|
||||
Output = output;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace CliFx.Models
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.Append(option.Name);
|
||||
buffer.Append(option.Alias);
|
||||
}
|
||||
|
||||
buffer.Append(']');
|
||||
|
||||
@@ -4,23 +4,23 @@ namespace CliFx.Models
|
||||
{
|
||||
public class CommandOptionInput
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Alias { get; }
|
||||
|
||||
public IReadOnlyList<string> Values { get; }
|
||||
|
||||
public CommandOptionInput(string name, IReadOnlyList<string> values)
|
||||
public CommandOptionInput(string alias, IReadOnlyList<string> values)
|
||||
{
|
||||
Name = name;
|
||||
Alias = alias;
|
||||
Values = values;
|
||||
}
|
||||
|
||||
public CommandOptionInput(string name, string value)
|
||||
: this(name, new[] {value})
|
||||
public CommandOptionInput(string alias, string value)
|
||||
: this(alias, new[] {value})
|
||||
{
|
||||
}
|
||||
|
||||
public CommandOptionInput(string name)
|
||||
: this(name, new string[0])
|
||||
public CommandOptionInput(string alias)
|
||||
: this(alias, new string[0])
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ namespace CliFx.Models
|
||||
if (x is null || y is null)
|
||||
return false;
|
||||
|
||||
return StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
|
||||
return StringComparer.OrdinalIgnoreCase.Equals(x.Alias, y.Alias) &&
|
||||
x.Values.SequenceEqual(y.Values, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder()
|
||||
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Add(obj.Alias, StringComparer.OrdinalIgnoreCase)
|
||||
.AddMany(obj.Values, StringComparer.Ordinal)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using CliFx.Internal;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
@@ -12,14 +10,14 @@ namespace CliFx.Models
|
||||
|
||||
public char? ShortName { get; }
|
||||
|
||||
public bool IsRequired { get; }
|
||||
|
||||
public string GroupName { get; }
|
||||
|
||||
public bool IsRequired { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public CommandOptionSchema(PropertyInfo property, string name, char? shortName,
|
||||
bool isRequired, string groupName, string description)
|
||||
string groupName, bool isRequired, string description)
|
||||
{
|
||||
Property = property;
|
||||
Name = name;
|
||||
|
||||
@@ -9,17 +9,14 @@ namespace CliFx.Models
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool IsDefault { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public IReadOnlyList<CommandOptionSchema> Options { get; }
|
||||
|
||||
public CommandSchema(Type type, string name, bool isDefault, string description, IReadOnlyList<CommandOptionSchema> options)
|
||||
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandOptionSchema> options)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
IsDefault = isDefault;
|
||||
Description = description;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace CliFx.Models
|
||||
|
||||
return x.Type == y.Type &&
|
||||
StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
|
||||
x.IsDefault == y.IsDefault &&
|
||||
StringComparer.Ordinal.Equals(x.Description, y.Description) &&
|
||||
x.Options.SequenceEqual(y.Options, CommandOptionSchemaEqualityComparer.Instance);
|
||||
}
|
||||
@@ -27,7 +26,6 @@ namespace CliFx.Models
|
||||
public int GetHashCode(CommandSchema obj) => new HashCodeBuilder()
|
||||
.Add(obj.Type)
|
||||
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Add(obj.IsDefault)
|
||||
.Add(obj.Description, StringComparer.Ordinal)
|
||||
.AddMany(obj.Options, CommandOptionSchemaEqualityComparer.Instance)
|
||||
.Build();
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
@@ -6,16 +7,66 @@ namespace CliFx.Models
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static CommandOptionInput GetOptionOrDefault(this CommandInput set, string name, char? shortName) =>
|
||||
set.Options.FirstOrDefault(o =>
|
||||
public static bool IsCommandSpecified(this CommandInput commandInput) => !commandInput.CommandName.IsNullOrWhiteSpace();
|
||||
|
||||
public static bool IsEmpty(this CommandInput commandInput) => !commandInput.IsCommandSpecified() && !commandInput.Options.Any();
|
||||
|
||||
public static bool IsHelpOption(this CommandOptionInput optionInput) =>
|
||||
string.Equals(optionInput.Alias, "help", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(optionInput.Alias, "h", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(optionInput.Alias, "?", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsVersionOption(this CommandOptionInput optionInput) =>
|
||||
string.Equals(optionInput.Alias, "version", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsHelpRequested(this CommandInput commandInput) => commandInput.Options.Any(o => o.IsHelpOption());
|
||||
|
||||
public static bool IsVersionRequested(this CommandInput commandInput) => commandInput.Options.Any(o => o.IsVersionOption());
|
||||
|
||||
public static bool IsDefault(this CommandSchema commandSchema) => commandSchema.Name.IsNullOrWhiteSpace();
|
||||
|
||||
public static CommandSchema FindByNameOrNull(this IEnumerable<CommandSchema> commandSchemas, string name) =>
|
||||
commandSchemas.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static IReadOnlyList<CommandSchema> FindSubCommandSchemas(this IEnumerable<CommandSchema> commandSchemas,
|
||||
string parentName)
|
||||
{
|
||||
if (!name.IsNullOrWhiteSpace() && string.Equals(o.Name, name, StringComparison.Ordinal))
|
||||
return true;
|
||||
// For a command with no name, every other command is its subcommand
|
||||
if (parentName.IsNullOrWhiteSpace())
|
||||
return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace()).ToArray();
|
||||
|
||||
if (shortName != null && o.Name.Length == 1 && o.Name.Single() == shortName)
|
||||
return true;
|
||||
// For a named command, commands that are prefixed by its name are its subcommands
|
||||
return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace())
|
||||
.Where(c => c.Name.StartsWith(parentName + " ", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
public static CommandOptionSchema FindByAliasOrNull(this IEnumerable<CommandOptionSchema> optionSchemas, string alias) =>
|
||||
optionSchemas.FirstOrDefault(o => o.GetAliases().Contains(alias, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
return false;
|
||||
});
|
||||
public static IReadOnlyList<string> GetAliases(this CommandOptionSchema optionSchema)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (!optionSchema.Name.IsNullOrWhiteSpace())
|
||||
result.Add(optionSchema.Name);
|
||||
|
||||
if (optionSchema.ShortName != null)
|
||||
result.Add(optionSchema.ShortName.Value.AsString());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetAliasesWithPrefixes(this CommandOptionSchema optionSchema)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (!optionSchema.Name.IsNullOrWhiteSpace())
|
||||
result.Add("--" + optionSchema.Name);
|
||||
|
||||
if (optionSchema.ShortName != null)
|
||||
result.Add("-" + optionSchema.ShortName.Value.AsString());
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/CommandFactory.cs
Normal file
10
CliFx/Services/CommandFactory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class CommandFactory : ICommandFactory
|
||||
{
|
||||
public ICommand CreateCommand(CommandSchema schema) => (ICommand) Activator.CreateInstance(schema.Type);
|
||||
}
|
||||
}
|
||||
141
CliFx/Services/CommandHelpTextBuilder.cs
Normal file
141
CliFx/Services/CommandHelpTextBuilder.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
// TODO: add color
|
||||
public class CommandHelpTextBuilder : ICommandHelpTextBuilder
|
||||
{
|
||||
// TODO: move to context?
|
||||
private string GetExeName() => Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()?.Location);
|
||||
|
||||
private void AddDescription(StringBuilder buffer, CommandSchema commands)
|
||||
{
|
||||
if (commands.Description.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Description:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.AppendLine(commands.Description);
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
private void AddUsage(StringBuilder buffer, CommandSchema command, IReadOnlyList<CommandSchema> subCommands)
|
||||
{
|
||||
buffer.AppendLine("Usage:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.Append(GetExeName());
|
||||
|
||||
if (!command.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append(command.Name);
|
||||
}
|
||||
|
||||
if (subCommands.Any())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append("[command]");
|
||||
}
|
||||
|
||||
if (command.Options.Any())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append("[options]");
|
||||
}
|
||||
|
||||
buffer.AppendLine().AppendLine();
|
||||
}
|
||||
|
||||
private void AddOptions(StringBuilder buffer, CommandSchema command)
|
||||
{
|
||||
if (!command.Options.Any())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Options:");
|
||||
|
||||
foreach (var option in command.Options)
|
||||
{
|
||||
buffer.Append(option.IsRequired ? "* " : " ");
|
||||
|
||||
buffer.Append(option.GetAliasesWithPrefixes().JoinToString("|"));
|
||||
|
||||
if (!option.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append(option.Description);
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
// Help option
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append("--help|-h");
|
||||
buffer.Append(" ");
|
||||
buffer.Append("Shows helps text.");
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
// Version option
|
||||
if (command.IsDefault())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append("--version");
|
||||
buffer.Append(" ");
|
||||
buffer.Append("Shows application version.");
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
private void AddSubCommands(StringBuilder buffer, IReadOnlyList<CommandSchema> subCommands)
|
||||
{
|
||||
if (!subCommands.Any())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Commands:");
|
||||
|
||||
foreach (var command in subCommands)
|
||||
{
|
||||
buffer.Append(" ");
|
||||
|
||||
buffer.Append(command.Name);
|
||||
|
||||
if (!command.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append(command.Description);
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
public string Build(IReadOnlyList<CommandSchema> commandSchemas, CommandSchema commandSchema)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
var subCommands = commandSchemas.FindSubCommandSchemas(commandSchema.Name);
|
||||
|
||||
AddDescription(buffer, commandSchema);
|
||||
AddUsage(buffer, commandSchema, subCommands);
|
||||
AddOptions(buffer, commandSchema);
|
||||
AddSubCommands(buffer, subCommands);
|
||||
|
||||
return buffer.ToString().Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
@@ -10,115 +9,44 @@ namespace CliFx.Services
|
||||
{
|
||||
public class CommandInitializer : ICommandInitializer
|
||||
{
|
||||
private readonly ITypeActivator _typeActivator;
|
||||
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
||||
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
|
||||
|
||||
public CommandInitializer(ITypeActivator typeActivator, ICommandSchemaResolver commandSchemaResolver,
|
||||
ICommandOptionInputConverter commandOptionInputConverter)
|
||||
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter)
|
||||
{
|
||||
_typeActivator = typeActivator;
|
||||
_commandSchemaResolver = commandSchemaResolver;
|
||||
_commandOptionInputConverter = commandOptionInputConverter;
|
||||
}
|
||||
|
||||
public CommandInitializer(ICommandSchemaResolver commandSchemaResolver)
|
||||
: this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter())
|
||||
{
|
||||
}
|
||||
|
||||
public CommandInitializer()
|
||||
: this(new CommandSchemaResolver())
|
||||
: this(new CommandOptionInputConverter())
|
||||
{
|
||||
}
|
||||
|
||||
private CommandSchema GetDefaultSchema(IReadOnlyList<CommandSchema> schemas)
|
||||
public void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input)
|
||||
{
|
||||
// Get command types marked as default
|
||||
var defaultSchemas = schemas.Where(t => t.IsDefault).ToArray();
|
||||
|
||||
// If there's only one type - return
|
||||
if (defaultSchemas.Length == 1)
|
||||
return defaultSchemas.Single();
|
||||
|
||||
// If there are multiple - throw
|
||||
if (defaultSchemas.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(CommandAttribute)} 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(CommandAttribute)} to the default command.");
|
||||
}
|
||||
|
||||
private CommandSchema GetSchemaByName(IReadOnlyList<CommandSchema> schemas, string name)
|
||||
{
|
||||
// Get command types with given name
|
||||
var matchingSchemas =
|
||||
schemas.Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
|
||||
// If there's only one type - return
|
||||
if (matchingSchemas.Length == 1)
|
||||
return matchingSchemas.Single();
|
||||
|
||||
// If there are multiple - throw
|
||||
if (matchingSchemas.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.");
|
||||
}
|
||||
|
||||
// TODO: refactor
|
||||
public ICommand InitializeCommand(CommandInput input)
|
||||
{
|
||||
var schemas = _commandSchemaResolver.ResolveAllSchemas();
|
||||
|
||||
// Get command type
|
||||
var schema = !input.CommandName.IsNullOrWhiteSpace()
|
||||
? GetSchemaByName(schemas, input.CommandName)
|
||||
: GetDefaultSchema(schemas);
|
||||
|
||||
// Activate command
|
||||
var command = (ICommand) _typeActivator.Activate(schema.Type);
|
||||
command.Context = new CommandContext(schemas, schema);
|
||||
|
||||
// Set command options
|
||||
var isGroupNameDetected = false;
|
||||
var groupName = default(string);
|
||||
var properties = new HashSet<CommandOptionSchema>();
|
||||
foreach (var option in input.Options)
|
||||
{
|
||||
var optionInfo = schema.Options.FirstOrDefault(p =>
|
||||
string.Equals(p.Name, option.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(p.ShortName?.AsString(), option.Name, StringComparison.OrdinalIgnoreCase));
|
||||
var optionSchema = schema.Options.FindByAliasOrNull(option.Alias);
|
||||
|
||||
if (optionInfo == null)
|
||||
if (optionSchema == null)
|
||||
continue;
|
||||
|
||||
if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase))
|
||||
if (isGroupNameDetected && !string.Equals(groupName, optionSchema.GroupName, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (!isGroupNameDetected)
|
||||
{
|
||||
groupName = optionInfo.GroupName;
|
||||
groupName = optionSchema.GroupName;
|
||||
isGroupNameDetected = true;
|
||||
}
|
||||
|
||||
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType);
|
||||
optionInfo.Property.SetValue(command, convertedValue);
|
||||
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionSchema.Property.PropertyType);
|
||||
optionSchema.Property.SetValue(command, convertedValue);
|
||||
|
||||
properties.Add(optionInfo);
|
||||
properties.Add(optionSchema);
|
||||
}
|
||||
|
||||
var unsetRequiredOptions = schema.Options
|
||||
@@ -128,10 +56,8 @@ namespace CliFx.Services
|
||||
.ToArray();
|
||||
|
||||
if (unsetRequiredOptions.Any())
|
||||
throw new CommandResolveException(
|
||||
throw new MissingCommandOptionException(
|
||||
$"Can't resolve command because one or more required properties were not set: {unsetRequiredOptions.Select(p => p.Name).JoinToString(", ")}");
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace CliFx.Services
|
||||
if (bool.TryParse(value, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to boolean.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to boolean.");
|
||||
}
|
||||
|
||||
// Char
|
||||
@@ -48,7 +48,7 @@ namespace CliFx.Services
|
||||
if (value.Length == 1)
|
||||
return value[0];
|
||||
|
||||
throw new CommandOptionConvertException(
|
||||
throw new CannotConvertCommandOptionException(
|
||||
$"Can't convert value [{value}] to char. The value is either empty or longer than one character.");
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace CliFx.Services
|
||||
if (sbyte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to sbyte.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to sbyte.");
|
||||
}
|
||||
|
||||
// Byte
|
||||
@@ -67,7 +67,7 @@ namespace CliFx.Services
|
||||
if (byte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to byte.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to byte.");
|
||||
}
|
||||
|
||||
// Short
|
||||
@@ -76,7 +76,7 @@ namespace CliFx.Services
|
||||
if (short.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to short.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to short.");
|
||||
}
|
||||
|
||||
// Ushort
|
||||
@@ -85,7 +85,7 @@ namespace CliFx.Services
|
||||
if (ushort.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to ushort.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ushort.");
|
||||
}
|
||||
|
||||
// Int
|
||||
@@ -94,7 +94,7 @@ namespace CliFx.Services
|
||||
if (int.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to int.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to int.");
|
||||
}
|
||||
|
||||
// Uint
|
||||
@@ -103,7 +103,7 @@ namespace CliFx.Services
|
||||
if (uint.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to uint.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to uint.");
|
||||
}
|
||||
|
||||
// Long
|
||||
@@ -112,7 +112,7 @@ namespace CliFx.Services
|
||||
if (long.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to long.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to long.");
|
||||
}
|
||||
|
||||
// Ulong
|
||||
@@ -121,7 +121,7 @@ namespace CliFx.Services
|
||||
if (ulong.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to ulong.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ulong.");
|
||||
}
|
||||
|
||||
// Float
|
||||
@@ -130,7 +130,7 @@ namespace CliFx.Services
|
||||
if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to float.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to float.");
|
||||
}
|
||||
|
||||
// Double
|
||||
@@ -139,7 +139,7 @@ namespace CliFx.Services
|
||||
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to double.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to double.");
|
||||
}
|
||||
|
||||
// Decimal
|
||||
@@ -148,7 +148,7 @@ namespace CliFx.Services
|
||||
if (decimal.TryParse(value, NumberStyles.Number, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to decimal.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to decimal.");
|
||||
}
|
||||
|
||||
// DateTime
|
||||
@@ -157,7 +157,7 @@ namespace CliFx.Services
|
||||
if (DateTime.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTime.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTime.");
|
||||
}
|
||||
|
||||
// DateTimeOffset
|
||||
@@ -166,7 +166,7 @@ namespace CliFx.Services
|
||||
if (DateTimeOffset.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTimeOffset.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTimeOffset.");
|
||||
}
|
||||
|
||||
// TimeSpan
|
||||
@@ -175,7 +175,7 @@ namespace CliFx.Services
|
||||
if (TimeSpan.TryParse(value, _formatProvider, out var result))
|
||||
return result;
|
||||
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to TimeSpan.");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to TimeSpan.");
|
||||
}
|
||||
|
||||
// Enum
|
||||
@@ -184,7 +184,7 @@ namespace CliFx.Services
|
||||
if (Enum.GetNames(targetType).Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||
return Enum.Parse(targetType, value, true);
|
||||
|
||||
throw new CommandOptionConvertException(
|
||||
throw new CannotConvertCommandOptionException(
|
||||
$"Can't convert value [{value}] to [{targetType}]. The value is not defined on the enum.");
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace CliFx.Services
|
||||
}
|
||||
|
||||
// Unknown type
|
||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}].");
|
||||
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to unrecognized type [{targetType}].");
|
||||
}
|
||||
|
||||
// TODO: refactor this
|
||||
@@ -226,7 +226,7 @@ namespace CliFx.Services
|
||||
if (targetType.IsAssignableFrom(underlyingType.MakeArrayType()))
|
||||
return option.Values.Select(v => ConvertValue(v, underlyingType)).ToArray().ToNonGenericArray(underlyingType);
|
||||
|
||||
throw new CommandOptionConvertException(
|
||||
throw new CannotConvertCommandOptionException(
|
||||
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||
}
|
||||
else if (option.Values.Count <= 1)
|
||||
@@ -239,7 +239,7 @@ namespace CliFx.Services
|
||||
else
|
||||
{
|
||||
// TODO: better exception
|
||||
throw new CommandOptionConvertException(
|
||||
throw new CannotConvertCommandOptionException(
|
||||
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||
{
|
||||
private readonly IReadOnlyList<Type> _sourceTypes;
|
||||
|
||||
public CommandSchemaResolver(IReadOnlyList<Type> sourceTypes)
|
||||
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
|
||||
{
|
||||
_sourceTypes = sourceTypes;
|
||||
var attribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||
|
||||
if (attribute == null)
|
||||
return null;
|
||||
|
||||
return new CommandOptionSchema(optionProperty,
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.GroupName,
|
||||
attribute.IsRequired, attribute.Description);
|
||||
}
|
||||
|
||||
public CommandSchemaResolver(IReadOnlyList<Assembly> sourceAssemblies)
|
||||
: this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray())
|
||||
// TODO: validate stuff like duplicate names, multiple default commands, etc
|
||||
public CommandSchema GetCommandSchema(Type commandType)
|
||||
{
|
||||
}
|
||||
if (!commandType.Implements(typeof(ICommand)))
|
||||
throw new ArgumentException($"Command type must implement {nameof(ICommand)}.", nameof(commandType));
|
||||
|
||||
public CommandSchemaResolver()
|
||||
: this(new[] {Assembly.GetEntryAssembly()})
|
||||
{
|
||||
}
|
||||
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
private IEnumerable<Type> GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand)));
|
||||
var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
|
||||
|
||||
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
||||
{
|
||||
var result = new List<CommandOptionSchema>();
|
||||
|
||||
foreach (var optionProperty in commandType.GetProperties())
|
||||
{
|
||||
var optionAttribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||
|
||||
if (optionAttribute == null)
|
||||
continue;
|
||||
|
||||
result.Add(new CommandOptionSchema(optionProperty,
|
||||
optionAttribute.Name,
|
||||
optionAttribute.ShortName,
|
||||
optionAttribute.IsRequired,
|
||||
optionAttribute.GroupName,
|
||||
optionAttribute.Description));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CommandSchema> ResolveAllSchemas()
|
||||
{
|
||||
var result = new List<CommandSchema>();
|
||||
|
||||
foreach (var commandType in GetCommandTypes())
|
||||
{
|
||||
var commandAttribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
if (commandAttribute == null)
|
||||
continue;
|
||||
|
||||
result.Add(new CommandSchema(commandType,
|
||||
commandAttribute.Name,
|
||||
commandAttribute.IsDefault,
|
||||
commandAttribute.Description,
|
||||
GetCommandOptionSchemas(commandType)));
|
||||
}
|
||||
|
||||
return result;
|
||||
return new CommandSchema(commandType,
|
||||
attribute?.Name,
|
||||
attribute?.Description,
|
||||
options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,42 @@
|
||||
using CliFx.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static void Write(this IConsoleWriter consoleWriter, string text) => consoleWriter.Write(new TextSpan(text));
|
||||
public static IReadOnlyList<CommandSchema> GetCommandSchemas(this ICommandSchemaResolver commandSchemaResolver,
|
||||
IEnumerable<Type> commandTypes) => commandTypes.Select(commandSchemaResolver.GetCommandSchema).ToArray();
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, string text) => consoleWriter.WriteLine(new TextSpan(text));
|
||||
public static void Write(this IConsoleWriter consoleWriter, string text) =>
|
||||
consoleWriter.Write(new TextSpan(text));
|
||||
|
||||
public static void Write(this IConsoleWriter consoleWriter, IFormattable formattable) =>
|
||||
consoleWriter.Write(formattable.ToString(null, CultureInfo.InvariantCulture));
|
||||
|
||||
public static void Write(this IConsoleWriter consoleWriter, object obj)
|
||||
{
|
||||
if (obj is IFormattable formattable)
|
||||
consoleWriter.Write(formattable);
|
||||
else
|
||||
consoleWriter.Write(obj.ToString());
|
||||
}
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, string text) =>
|
||||
consoleWriter.WriteLine(new TextSpan(text));
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, IFormattable formattable) =>
|
||||
consoleWriter.WriteLine(formattable.ToString(null, CultureInfo.InvariantCulture));
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, object obj)
|
||||
{
|
||||
if (obj is IFormattable formattable)
|
||||
consoleWriter.WriteLine(formattable);
|
||||
else
|
||||
consoleWriter.WriteLine(obj.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
// TODO: add color
|
||||
public class HelpTextBuilder : IHelpTextBuilder
|
||||
{
|
||||
// TODO: move to context?
|
||||
private string GetExeName() => Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()?.Location);
|
||||
|
||||
// TODO: move to context?
|
||||
private string GetVersionText() => Assembly.GetEntryAssembly()?.GetName().Version.ToString();
|
||||
|
||||
private IReadOnlyList<string> GetOptionIdentifiers(CommandOptionSchema option)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (option.ShortName != null)
|
||||
result.Add("-" + option.ShortName.Value);
|
||||
|
||||
if (!option.Name.IsNullOrWhiteSpace())
|
||||
result.Add("--" + option.Name);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void AddDescription(StringBuilder buffer, CommandContext context)
|
||||
{
|
||||
if (context.CommandSchema.Description.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Description:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.AppendLine(context.CommandSchema.Description);
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
private void AddUsage(StringBuilder buffer, CommandContext context)
|
||||
{
|
||||
buffer.AppendLine("Usage:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.Append(GetExeName());
|
||||
|
||||
if (!context.CommandSchema.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append(context.CommandSchema.Name);
|
||||
}
|
||||
|
||||
if (context.CommandSchema.Options.Any())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append("[options]");
|
||||
}
|
||||
|
||||
buffer.AppendLine().AppendLine();
|
||||
}
|
||||
|
||||
private void AddOptions(StringBuilder buffer, CommandContext context)
|
||||
{
|
||||
if (!context.CommandSchema.Options.Any())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Options:");
|
||||
|
||||
foreach (var option in context.CommandSchema.Options)
|
||||
{
|
||||
buffer.Append(option.IsRequired ? " * " : " ");
|
||||
|
||||
buffer.Append(GetOptionIdentifiers(option).JoinToString("|"));
|
||||
|
||||
if (!option.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append(option.Description);
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
public string Build(CommandContext context)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
AddDescription(buffer, context);
|
||||
AddUsage(buffer, context);
|
||||
AddOptions(buffer, context);
|
||||
|
||||
// TODO: add default command help
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandFactory.cs
Normal file
10
CliFx/Services/ICommandFactory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandFactory
|
||||
{
|
||||
ICommand CreateCommand(CommandSchema schema);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandHelpTextBuilder.cs
Normal file
10
CliFx/Services/ICommandHelpTextBuilder.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandHelpTextBuilder
|
||||
{
|
||||
string Build(IReadOnlyList<CommandSchema> commandSchemas, CommandSchema commandSchema);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@ namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandInitializer
|
||||
{
|
||||
ICommand InitializeCommand(CommandInput input);
|
||||
void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandSchemaResolver
|
||||
{
|
||||
IReadOnlyList<CommandSchema> ResolveAllSchemas();
|
||||
CommandSchema GetCommandSchema(Type commandType);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface IHelpTextBuilder
|
||||
{
|
||||
string Build(CommandContext context);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ITypeActivator
|
||||
{
|
||||
object Activate(Type type);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class TypeActivator : ITypeActivator
|
||||
{
|
||||
public object Activate(Type type) => Activator.CreateInstance(type);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ CliFx is a powerful framework for building command line applications.
|
||||
## Features
|
||||
|
||||
- ...to be added with a stable release...
|
||||
- Targets .NET Framework 4.5+ and .NET Standard 2.0+
|
||||
- Targets .NET Framework 4.6+ and .NET Standard 2.0+
|
||||
- No external dependencies
|
||||
|
||||
## Usage
|
||||
|
||||
Reference in New Issue
Block a user