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>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net46</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Version>1.2.3.4</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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;
|
||||||
using System.Globalization;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
{
|
{
|
||||||
[Command("log", Description = "Calculate the logarithm of a value.")]
|
[Command("log", Description = "Calculates the logarithm of a value.")]
|
||||||
public class LogCommand : Command
|
public class LogCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
||||||
public double Value { get; set; }
|
public double Value { get; set; }
|
||||||
@@ -15,12 +15,12 @@ namespace CliFx.Tests.Dummy.Commands
|
|||||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||||
public double Base { get; set; } = 10;
|
public double Base { get; set; } = 10;
|
||||||
|
|
||||||
protected override ExitCode Process()
|
public Task ExecuteAsync(CommandContext context)
|
||||||
{
|
{
|
||||||
var result = Math.Log(Value, Base);
|
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.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
{
|
{
|
||||||
[Command("add", Description = "Calculate the sum of all input values.")]
|
[Command("sum", Description = "Calculates the sum of all input values.")]
|
||||||
public class AddCommand : Command
|
public class SumCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
|
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
|
||||||
public IReadOnlyList<double> Values { get; set; }
|
public IReadOnlyList<double> Values { get; set; }
|
||||||
|
|
||||||
protected override ExitCode Process()
|
public Task ExecuteAsync(CommandContext context)
|
||||||
{
|
{
|
||||||
var result = Values.Sum();
|
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.Attributes;
|
||||||
|
using CliFx.Exceptions;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace CliFx.Tests
|
namespace CliFx.Tests
|
||||||
@@ -9,32 +11,187 @@ namespace CliFx.Tests
|
|||||||
public partial class CliApplicationTests
|
public partial class CliApplicationTests
|
||||||
{
|
{
|
||||||
[Command]
|
[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]
|
[TestFixture]
|
||||||
public partial class CliApplicationTests
|
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]
|
[Test]
|
||||||
public async Task RunAsync_Test()
|
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||||
|
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var application = new CliApplication(
|
var application = new CliApplication(commandTypes);
|
||||||
new CommandInputParser(),
|
|
||||||
new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})));
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCodeValue = await application.RunAsync();
|
var exitCodeValue = await application.RunAsync(commandLineArguments);
|
||||||
|
|
||||||
// Assert
|
// 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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net46</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
<CollectCoverage>true</CollectCoverage>
|
<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 System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
@@ -10,7 +11,6 @@ namespace CliFx.Tests
|
|||||||
{
|
{
|
||||||
public partial class CommandInitializerTests
|
public partial class CommandInitializerTests
|
||||||
{
|
{
|
||||||
[Command]
|
|
||||||
public class TestCommand : ICommand
|
public class TestCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("int", 'i', IsRequired = true)]
|
[CommandOption("int", 'i', IsRequired = true)]
|
||||||
@@ -22,9 +22,7 @@ namespace CliFx.Tests
|
|||||||
[CommandOption("bool", 'b', GroupName = "other-group")]
|
[CommandOption("bool", 'b', GroupName = "other-group")]
|
||||||
public bool BoolOption { get; set; }
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
public CommandContext Context { get; set; }
|
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<ExitCode> ExecuteAsync() => throw new System.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()
|
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_IsRequired()
|
||||||
{
|
{
|
||||||
yield return new TestCaseData(CommandInput.Empty);
|
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]
|
[Test]
|
||||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))]
|
[TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))]
|
||||||
public void InitializeCommand_IsRequired_Test(CommandInput commandInput)
|
public void InitializeCommand_IsRequired_Test(CommandInput commandInput)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
|
var initializer = new CommandInitializer();
|
||||||
|
|
||||||
// Act & Assert
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
@@ -10,8 +11,10 @@ namespace CliFx.Tests
|
|||||||
{
|
{
|
||||||
public partial class CommandSchemaResolverTests
|
public partial class CommandSchemaResolverTests
|
||||||
{
|
{
|
||||||
[Command(Description = "Command description")]
|
[Command("Command name", Description = "Command description")]
|
||||||
public class TestCommand : ICommand
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||||
|
private class TestCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("option-a", 'a', GroupName = "Group 1")]
|
[CommandOption("option-a", 'a', GroupName = "Group 1")]
|
||||||
public int OptionA { get; set; }
|
public int OptionA { get; set; }
|
||||||
@@ -22,9 +25,7 @@ namespace CliFx.Tests
|
|||||||
[CommandOption("option-c", Description = "Option C description")]
|
[CommandOption("option-c", Description = "Option C description")]
|
||||||
public bool OptionC { get; set; }
|
public bool OptionC { get; set; }
|
||||||
|
|
||||||
public CommandContext Context { get; set; }
|
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<ExitCode> ExecuteAsync() => throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,36 +35,32 @@ namespace CliFx.Tests
|
|||||||
private static IEnumerable<TestCaseData> GetTestCases_ResolveAllSchemas()
|
private static IEnumerable<TestCaseData> GetTestCases_ResolveAllSchemas()
|
||||||
{
|
{
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] {typeof(TestCommand)},
|
typeof(TestCommand),
|
||||||
new[]
|
new CommandSchema(typeof(TestCommand), "Command name", "Command description",
|
||||||
{
|
|
||||||
new CommandSchema(typeof(TestCommand),
|
|
||||||
null, true, "Command description",
|
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)),
|
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)),
|
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)),
|
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]
|
[Test]
|
||||||
[TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))]
|
[TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))]
|
||||||
public void ResolveAllSchemas_Test(IReadOnlyList<Type> sourceTypes, IReadOnlyList<CommandSchema> expectedSchemas)
|
public void GetCommandSchema_Test(Type commandType, CommandSchema expectedSchema)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var resolver = new CommandSchemaResolver(sourceTypes);
|
var resolver = new CommandSchemaResolver();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var schemas = resolver.ResolveAllSchemas();
|
var schema = resolver.GetCommandSchema(commandType);
|
||||||
|
|
||||||
// Assert
|
// 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]
|
[Test]
|
||||||
[TestCase("", "Hello world")]
|
[TestCase("", "Hello world")]
|
||||||
[TestCase("-t .NET", "Hello .NET")]
|
[TestCase("-t .NET", "Hello .NET")]
|
||||||
[TestCase("-e", "Hello world!!!")]
|
[TestCase("-e", "Hello world!")]
|
||||||
[TestCase("add -v 1 2", "3")]
|
[TestCase("sum -v 1 2", "3")]
|
||||||
[TestCase("add -v 2.75 3.6 4.18", "10.53")]
|
[TestCase("sum -v 2.75 3.6 4.18", "10.53")]
|
||||||
[TestCase("add -v 4 -v 16", "20")]
|
[TestCase("sum -v 4 -v 16", "20")]
|
||||||
|
[TestCase("sum --values 2 5 --values 3", "10")]
|
||||||
[TestCase("log -v 100", "2")]
|
[TestCase("log -v 100", "2")]
|
||||||
[TestCase("log --value 256 --base 2", "8")]
|
[TestCase("log --value 256 --base 2", "8")]
|
||||||
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
|
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
|
||||||
{
|
{
|
||||||
// Act
|
// Arrange & Act
|
||||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
var result = await Cli.Wrap(DummyFilePath)
|
||||||
|
.SetArguments(arguments)
|
||||||
|
.EnableExitCodeValidation()
|
||||||
|
.EnableStandardErrorValidation()
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Multiple(() =>
|
|
||||||
{
|
|
||||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
|
||||||
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
|
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
|
||||||
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("--version")]
|
[TestCase("--version")]
|
||||||
public async Task CliApplication_RunAsync_Version_Test(string arguments)
|
public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments)
|
||||||
{
|
{
|
||||||
// Act
|
// Arrange & Act
|
||||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
var result = await Cli.Wrap(DummyFilePath)
|
||||||
|
.SetArguments(arguments)
|
||||||
|
.EnableExitCodeValidation()
|
||||||
|
.EnableStandardErrorValidation()
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Multiple(() =>
|
|
||||||
{
|
|
||||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
|
||||||
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout");
|
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout");
|
||||||
Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("--help")]
|
[TestCase("--help")]
|
||||||
[TestCase("-h")]
|
[TestCase("-h")]
|
||||||
[TestCase("add -h")]
|
[TestCase("sum -h")]
|
||||||
[TestCase("add --help")]
|
[TestCase("sum --help")]
|
||||||
[TestCase("log -h")]
|
[TestCase("log -h")]
|
||||||
[TestCase("log --help")]
|
[TestCase("log --help")]
|
||||||
public async Task CliApplication_RunAsync_Help_Test(string arguments)
|
public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments)
|
||||||
{
|
{
|
||||||
// Act
|
// Arrange & Act
|
||||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
var result = await Cli.Wrap(DummyFilePath)
|
||||||
|
.SetArguments(arguments)
|
||||||
|
.EnableExitCodeValidation()
|
||||||
|
.EnableStandardErrorValidation()
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Multiple(() =>
|
|
||||||
{
|
|
||||||
Assert.That(result.ExitCode, Is.Zero, "Exit code");
|
|
||||||
Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout");
|
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 string Description { get; set; }
|
||||||
|
|
||||||
public bool IsDefault => Name.IsNullOrWhiteSpace();
|
|
||||||
|
|
||||||
public CommandAttribute(string name)
|
public CommandAttribute(string name)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ namespace CliFx.Attributes
|
|||||||
|
|
||||||
public char? ShortName { get; }
|
public char? ShortName { get; }
|
||||||
|
|
||||||
public bool IsRequired { get; set; }
|
|
||||||
|
|
||||||
public string GroupName { get; set; }
|
public string GroupName { get; set; }
|
||||||
|
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
public CommandOptionAttribute(string name, char? shortName)
|
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 System.Threading.Tasks;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
public class CliApplication : ICliApplication
|
public partial class CliApplication : ICliApplication
|
||||||
{
|
{
|
||||||
|
private readonly IReadOnlyList<Type> _commandTypes;
|
||||||
private readonly ICommandInputParser _commandInputParser;
|
private readonly ICommandInputParser _commandInputParser;
|
||||||
|
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
||||||
|
private readonly ICommandFactory _commandFactory;
|
||||||
private readonly ICommandInitializer _commandInitializer;
|
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;
|
_commandInputParser = commandInputParser;
|
||||||
|
_commandSchemaResolver = commandSchemaResolver;
|
||||||
|
_commandFactory = commandFactory;
|
||||||
_commandInitializer = commandInitializer;
|
_commandInitializer = commandInitializer;
|
||||||
|
_commandHelpTextBuilder = commandHelpTextBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CliApplication(IReadOnlyList<Type> commandTypes)
|
||||||
|
: this(commandTypes,
|
||||||
|
new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(),
|
||||||
|
new CommandInitializer(), new CommandHelpTextBuilder())
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public CliApplication()
|
public CliApplication()
|
||||||
: this(new CommandInputParser(), new CommandInitializer())
|
: this(GetDefaultCommandTypes())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||||
{
|
{
|
||||||
var input = _commandInputParser.ParseInput(commandLineArguments);
|
var stdOut = ConsoleWriter.GetStandardOutput();
|
||||||
var command = _commandInitializer.InitializeCommand(input);
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<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
|
public interface ICommand
|
||||||
{
|
{
|
||||||
CommandContext Context { get; set; }
|
Task ExecuteAsync(CommandContext context);
|
||||||
|
|
||||||
Task<ExitCode> ExecuteAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,49 +11,12 @@ namespace CliFx.Internal
|
|||||||
|
|
||||||
public static string AsString(this char c) => new string(c, 1);
|
public static string AsString(this char c) => new string(c, 1);
|
||||||
|
|
||||||
public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
|
||||||
{
|
|
||||||
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) =>
|
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
||||||
dic.TryGetValue(key, out var result) ? result : default;
|
dic.TryGetValue(key, out var result) ? result : default;
|
||||||
|
|
||||||
public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
|
||||||
{
|
|
||||||
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 bool IsEnumerable(this Type type) =>
|
public static bool IsEnumerable(this Type type) =>
|
||||||
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
|
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
|
||||||
@@ -79,5 +42,7 @@ namespace CliFx.Internal
|
|||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,29 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx.Models
|
namespace CliFx.Models
|
||||||
{
|
{
|
||||||
public class CommandContext
|
public class CommandContext
|
||||||
{
|
{
|
||||||
|
public CommandInput CommandInput { get; }
|
||||||
|
|
||||||
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { 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;
|
AvailableCommandSchemas = availableCommandSchemas;
|
||||||
CommandSchema = commandSchema;
|
MatchingCommandSchema = matchingCommandSchema;
|
||||||
|
Output = output;
|
||||||
|
Error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ namespace CliFx.Models
|
|||||||
|
|
||||||
foreach (var option in Options)
|
foreach (var option in Options)
|
||||||
{
|
{
|
||||||
buffer.Append(option.Name);
|
buffer.Append(option.Alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.Append(']');
|
buffer.Append(']');
|
||||||
|
|||||||
@@ -4,23 +4,23 @@ namespace CliFx.Models
|
|||||||
{
|
{
|
||||||
public class CommandOptionInput
|
public class CommandOptionInput
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Alias { get; }
|
||||||
|
|
||||||
public IReadOnlyList<string> Values { 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;
|
Values = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandOptionInput(string name, string value)
|
public CommandOptionInput(string alias, string value)
|
||||||
: this(name, new[] {value})
|
: this(alias, new[] {value})
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandOptionInput(string name)
|
public CommandOptionInput(string alias)
|
||||||
: this(name, new string[0])
|
: this(alias, new string[0])
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ namespace CliFx.Models
|
|||||||
if (x is null || y is null)
|
if (x is null || y is null)
|
||||||
return false;
|
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);
|
x.Values.SequenceEqual(y.Values, StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder()
|
public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder()
|
||||||
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
|
.Add(obj.Alias, StringComparer.OrdinalIgnoreCase)
|
||||||
.AddMany(obj.Values, StringComparer.Ordinal)
|
.AddMany(obj.Values, StringComparer.Ordinal)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
|
||||||
using CliFx.Internal;
|
|
||||||
|
|
||||||
namespace CliFx.Models
|
namespace CliFx.Models
|
||||||
{
|
{
|
||||||
@@ -12,14 +10,14 @@ namespace CliFx.Models
|
|||||||
|
|
||||||
public char? ShortName { get; }
|
public char? ShortName { get; }
|
||||||
|
|
||||||
public bool IsRequired { get; }
|
|
||||||
|
|
||||||
public string GroupName { get; }
|
public string GroupName { get; }
|
||||||
|
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
|
|
||||||
public CommandOptionSchema(PropertyInfo property, string name, char? shortName,
|
public CommandOptionSchema(PropertyInfo property, string name, char? shortName,
|
||||||
bool isRequired, string groupName, string description)
|
string groupName, bool isRequired, string description)
|
||||||
{
|
{
|
||||||
Property = property;
|
Property = property;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ namespace CliFx.Models
|
|||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public bool IsDefault { get; }
|
|
||||||
|
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
|
|
||||||
public IReadOnlyList<CommandOptionSchema> Options { 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;
|
Type = type;
|
||||||
Name = name;
|
Name = name;
|
||||||
IsDefault = isDefault;
|
|
||||||
Description = description;
|
Description = description;
|
||||||
Options = options;
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ namespace CliFx.Models
|
|||||||
|
|
||||||
return x.Type == y.Type &&
|
return x.Type == y.Type &&
|
||||||
StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
|
StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
|
||||||
x.IsDefault == y.IsDefault &&
|
|
||||||
StringComparer.Ordinal.Equals(x.Description, y.Description) &&
|
StringComparer.Ordinal.Equals(x.Description, y.Description) &&
|
||||||
x.Options.SequenceEqual(y.Options, CommandOptionSchemaEqualityComparer.Instance);
|
x.Options.SequenceEqual(y.Options, CommandOptionSchemaEqualityComparer.Instance);
|
||||||
}
|
}
|
||||||
@@ -27,7 +26,6 @@ namespace CliFx.Models
|
|||||||
public int GetHashCode(CommandSchema obj) => new HashCodeBuilder()
|
public int GetHashCode(CommandSchema obj) => new HashCodeBuilder()
|
||||||
.Add(obj.Type)
|
.Add(obj.Type)
|
||||||
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
|
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Add(obj.IsDefault)
|
|
||||||
.Add(obj.Description, StringComparer.Ordinal)
|
.Add(obj.Description, StringComparer.Ordinal)
|
||||||
.AddMany(obj.Options, CommandOptionSchemaEqualityComparer.Instance)
|
.AddMany(obj.Options, CommandOptionSchemaEqualityComparer.Instance)
|
||||||
.Build();
|
.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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal;
|
||||||
|
|
||||||
@@ -6,16 +7,66 @@ namespace CliFx.Models
|
|||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
public static CommandOptionInput GetOptionOrDefault(this CommandInput set, string name, char? shortName) =>
|
public static bool IsCommandSpecified(this CommandInput commandInput) => !commandInput.CommandName.IsNullOrWhiteSpace();
|
||||||
set.Options.FirstOrDefault(o =>
|
|
||||||
|
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))
|
// For a command with no name, every other command is its subcommand
|
||||||
return true;
|
if (parentName.IsNullOrWhiteSpace())
|
||||||
|
return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace()).ToArray();
|
||||||
|
|
||||||
if (shortName != null && o.Name.Length == 1 && o.Name.Single() == shortName)
|
// For a named command, commands that are prefixed by its name are its subcommands
|
||||||
return true;
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
@@ -10,115 +9,44 @@ namespace CliFx.Services
|
|||||||
{
|
{
|
||||||
public class CommandInitializer : ICommandInitializer
|
public class CommandInitializer : ICommandInitializer
|
||||||
{
|
{
|
||||||
private readonly ITypeActivator _typeActivator;
|
|
||||||
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
|
||||||
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
|
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
|
||||||
|
|
||||||
public CommandInitializer(ITypeActivator typeActivator, ICommandSchemaResolver commandSchemaResolver,
|
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter)
|
||||||
ICommandOptionInputConverter commandOptionInputConverter)
|
|
||||||
{
|
{
|
||||||
_typeActivator = typeActivator;
|
|
||||||
_commandSchemaResolver = commandSchemaResolver;
|
|
||||||
_commandOptionInputConverter = commandOptionInputConverter;
|
_commandOptionInputConverter = commandOptionInputConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandInitializer(ICommandSchemaResolver commandSchemaResolver)
|
|
||||||
: this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandInitializer()
|
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
|
// Set command options
|
||||||
var isGroupNameDetected = false;
|
var isGroupNameDetected = false;
|
||||||
var groupName = default(string);
|
var groupName = default(string);
|
||||||
var properties = new HashSet<CommandOptionSchema>();
|
var properties = new HashSet<CommandOptionSchema>();
|
||||||
foreach (var option in input.Options)
|
foreach (var option in input.Options)
|
||||||
{
|
{
|
||||||
var optionInfo = schema.Options.FirstOrDefault(p =>
|
var optionSchema = schema.Options.FindByAliasOrNull(option.Alias);
|
||||||
string.Equals(p.Name, option.Name, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(p.ShortName?.AsString(), option.Name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (optionInfo == null)
|
if (optionSchema == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase))
|
if (isGroupNameDetected && !string.Equals(groupName, optionSchema.GroupName, StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!isGroupNameDetected)
|
if (!isGroupNameDetected)
|
||||||
{
|
{
|
||||||
groupName = optionInfo.GroupName;
|
groupName = optionSchema.GroupName;
|
||||||
isGroupNameDetected = true;
|
isGroupNameDetected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType);
|
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionSchema.Property.PropertyType);
|
||||||
optionInfo.Property.SetValue(command, convertedValue);
|
optionSchema.Property.SetValue(command, convertedValue);
|
||||||
|
|
||||||
properties.Add(optionInfo);
|
properties.Add(optionSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
var unsetRequiredOptions = schema.Options
|
var unsetRequiredOptions = schema.Options
|
||||||
@@ -128,10 +56,8 @@ namespace CliFx.Services
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (unsetRequiredOptions.Any())
|
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(", ")}");
|
$"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))
|
if (bool.TryParse(value, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to boolean.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to boolean.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Char
|
// Char
|
||||||
@@ -48,7 +48,7 @@ namespace CliFx.Services
|
|||||||
if (value.Length == 1)
|
if (value.Length == 1)
|
||||||
return value[0];
|
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.");
|
$"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))
|
if (sbyte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to sbyte.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to sbyte.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Byte
|
// Byte
|
||||||
@@ -67,7 +67,7 @@ namespace CliFx.Services
|
|||||||
if (byte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (byte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to byte.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to byte.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short
|
// Short
|
||||||
@@ -76,7 +76,7 @@ namespace CliFx.Services
|
|||||||
if (short.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (short.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to short.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to short.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ushort
|
// Ushort
|
||||||
@@ -85,7 +85,7 @@ namespace CliFx.Services
|
|||||||
if (ushort.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (ushort.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to ushort.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ushort.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int
|
// Int
|
||||||
@@ -94,7 +94,7 @@ namespace CliFx.Services
|
|||||||
if (int.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (int.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to int.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to int.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uint
|
// Uint
|
||||||
@@ -103,7 +103,7 @@ namespace CliFx.Services
|
|||||||
if (uint.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (uint.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to uint.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to uint.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long
|
// Long
|
||||||
@@ -112,7 +112,7 @@ namespace CliFx.Services
|
|||||||
if (long.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (long.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to long.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to long.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ulong
|
// Ulong
|
||||||
@@ -121,7 +121,7 @@ namespace CliFx.Services
|
|||||||
if (ulong.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
if (ulong.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to ulong.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ulong.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float
|
// Float
|
||||||
@@ -130,7 +130,7 @@ namespace CliFx.Services
|
|||||||
if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to float.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to float.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double
|
// Double
|
||||||
@@ -139,7 +139,7 @@ namespace CliFx.Services
|
|||||||
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to double.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to double.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decimal
|
// Decimal
|
||||||
@@ -148,7 +148,7 @@ namespace CliFx.Services
|
|||||||
if (decimal.TryParse(value, NumberStyles.Number, _formatProvider, out var result))
|
if (decimal.TryParse(value, NumberStyles.Number, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to decimal.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to decimal.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// DateTime
|
// DateTime
|
||||||
@@ -157,7 +157,7 @@ namespace CliFx.Services
|
|||||||
if (DateTime.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
if (DateTime.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTime.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTime.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// DateTimeOffset
|
// DateTimeOffset
|
||||||
@@ -166,7 +166,7 @@ namespace CliFx.Services
|
|||||||
if (DateTimeOffset.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
if (DateTimeOffset.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTimeOffset.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTimeOffset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeSpan
|
// TimeSpan
|
||||||
@@ -175,7 +175,7 @@ namespace CliFx.Services
|
|||||||
if (TimeSpan.TryParse(value, _formatProvider, out var result))
|
if (TimeSpan.TryParse(value, _formatProvider, out var result))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
throw new CommandOptionConvertException($"Can't convert value [{value}] to TimeSpan.");
|
throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to TimeSpan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enum
|
// Enum
|
||||||
@@ -184,7 +184,7 @@ namespace CliFx.Services
|
|||||||
if (Enum.GetNames(targetType).Contains(value, StringComparer.OrdinalIgnoreCase))
|
if (Enum.GetNames(targetType).Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||||
return Enum.Parse(targetType, value, true);
|
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.");
|
$"Can't convert value [{value}] to [{targetType}]. The value is not defined on the enum.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ namespace CliFx.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type
|
// 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
|
// TODO: refactor this
|
||||||
@@ -226,7 +226,7 @@ namespace CliFx.Services
|
|||||||
if (targetType.IsAssignableFrom(underlyingType.MakeArrayType()))
|
if (targetType.IsAssignableFrom(underlyingType.MakeArrayType()))
|
||||||
return option.Values.Select(v => ConvertValue(v, underlyingType)).ToArray().ToNonGenericArray(underlyingType);
|
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}].");
|
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||||
}
|
}
|
||||||
else if (option.Values.Count <= 1)
|
else if (option.Values.Count <= 1)
|
||||||
@@ -239,7 +239,7 @@ namespace CliFx.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: better exception
|
// TODO: better exception
|
||||||
throw new CommandOptionConvertException(
|
throw new CannotConvertCommandOptionException(
|
||||||
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
|
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,42 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Internal;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
|
|
||||||
namespace CliFx.Services
|
namespace CliFx.Services
|
||||||
{
|
{
|
||||||
public class CommandSchemaResolver : ICommandSchemaResolver
|
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyList<Type> _sourceTypes;
|
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
|
||||||
|
|
||||||
public CommandSchemaResolver(IReadOnlyList<Type> sourceTypes)
|
|
||||||
{
|
{
|
||||||
_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)
|
// TODO: validate stuff like duplicate names, multiple default commands, etc
|
||||||
: this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray())
|
public CommandSchema GetCommandSchema(Type commandType)
|
||||||
{
|
{
|
||||||
}
|
if (!commandType.Implements(typeof(ICommand)))
|
||||||
|
throw new ArgumentException($"Command type must implement {nameof(ICommand)}.", nameof(commandType));
|
||||||
|
|
||||||
public CommandSchemaResolver()
|
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||||
: this(new[] {Assembly.GetEntryAssembly()})
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
return new CommandSchema(commandType,
|
||||||
{
|
attribute?.Name,
|
||||||
var result = new List<CommandOptionSchema>();
|
attribute?.Description,
|
||||||
|
options);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
namespace CliFx.Services
|
||||||
{
|
{
|
||||||
public static class Extensions
|
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
|
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;
|
using CliFx.Models;
|
||||||
|
|
||||||
namespace CliFx.Services
|
namespace CliFx.Services
|
||||||
{
|
{
|
||||||
public interface ICommandSchemaResolver
|
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
|
## Features
|
||||||
|
|
||||||
- ...to be added with a stable release...
|
- ...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
|
- No external dependencies
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
Reference in New Issue
Block a user