diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
index dcae554..e71679a 100644
--- a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
+++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
@@ -2,8 +2,9 @@
Exe
- net45
+ net46
latest
+ 1.2.3.4
diff --git a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs
deleted file mode 100644
index 5648c02..0000000
--- a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/GreeterCommand.cs b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs
new file mode 100644
index 0000000..c1cf18b
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs
index b61e357..8e3a7ef 100644
--- a/CliFx.Tests.Dummy/Commands/LogCommand.cs
+++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs
@@ -1,13 +1,13 @@
using System;
-using System.Globalization;
+using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Models;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
- [Command("log", Description = "Calculate the logarithm of a value.")]
- public class LogCommand : Command
+ [Command("log", Description = "Calculates the logarithm of a value.")]
+ public class LogCommand : ICommand
{
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
public double Value { get; set; }
@@ -15,12 +15,12 @@ namespace CliFx.Tests.Dummy.Commands
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; set; } = 10;
- protected override ExitCode Process()
+ public Task ExecuteAsync(CommandContext context)
{
var result = Math.Log(Value, Base);
- Output.WriteLine(result.ToString(CultureInfo.InvariantCulture));
+ context.Output.WriteLine(result);
- return ExitCode.Success;
+ return Task.CompletedTask;
}
}
}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/AddCommand.cs b/CliFx.Tests.Dummy/Commands/SumCommand.cs
similarity index 55%
rename from CliFx.Tests.Dummy/Commands/AddCommand.cs
rename to CliFx.Tests.Dummy/Commands/SumCommand.cs
index 06e6d26..dea684b 100644
--- a/CliFx.Tests.Dummy/Commands/AddCommand.cs
+++ b/CliFx.Tests.Dummy/Commands/SumCommand.cs
@@ -1,24 +1,24 @@
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
+using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Models;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
- [Command("add", Description = "Calculate the sum of all input values.")]
- public class AddCommand : Command
+ [Command("sum", Description = "Calculates the sum of all input values.")]
+ public class SumCommand : ICommand
{
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
public IReadOnlyList Values { get; set; }
- protected override ExitCode Process()
+ public Task ExecuteAsync(CommandContext context)
{
var result = Values.Sum();
- Output.WriteLine(result.ToString(CultureInfo.InvariantCulture));
+ context.Output.WriteLine(result);
- return ExitCode.Success;
+ return Task.CompletedTask;
}
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs
index f0d5bee..fbbaa9c 100644
--- a/CliFx.Tests/CliApplicationTests.cs
+++ b/CliFx.Tests/CliApplicationTests.cs
@@ -1,7 +1,9 @@
-using System.Threading.Tasks;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
using CliFx.Attributes;
+using CliFx.Exceptions;
using CliFx.Models;
-using CliFx.Services;
using NUnit.Framework;
namespace CliFx.Tests
@@ -9,32 +11,187 @@ namespace CliFx.Tests
public partial class CliApplicationTests
{
[Command]
- public class TestCommand : ICommand
+ private class TestDefaultCommand : ICommand
{
- public static ExitCode ExitCode { get; } = new ExitCode(13);
+ public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
+ }
- public CommandContext Context { get; set; }
+ [Command("command")]
+ private class TestNamedCommand : ICommand
+ {
+ public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
+ }
- public Task ExecuteAsync() => Task.FromResult(ExitCode);
+ [Command("faulty-command")]
+ private class FaultyCommand : ICommand
+ {
+ public Task ExecuteAsync(CommandContext context) => Task.FromException(new CommandErrorException(-1337));
}
}
[TestFixture]
public partial class CliApplicationTests
{
+ private static IEnumerable 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 GetTestCases_RunAsync_Negative()
+ {
+ // Specified command is not defined
+
+ yield return new TestCaseData(
+ new Type[0],
+ new[] {"command"}
+ );
+
+ yield return new TestCaseData(
+ new Type[0],
+ new[] {"command", "--help"}
+ );
+
+ yield return new TestCaseData(
+ new Type[0],
+ new[] {"command", "-h"}
+ );
+
+ yield return new TestCaseData(
+ new Type[0],
+ new[] {"command", "-?"}
+ );
+
+ // Specified a faulty command
+
+ yield return new TestCaseData(
+ new[] {typeof(FaultyCommand)},
+ new[] {"faulty-command"}
+ );
+ }
+
[Test]
- public async Task RunAsync_Test()
+ [TestCaseSource(nameof(GetTestCases_RunAsync))]
+ public async Task RunAsync_Test(IReadOnlyList commandTypes, IReadOnlyList commandLineArguments)
{
// Arrange
- var application = new CliApplication(
- new CommandInputParser(),
- new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})));
+ var application = new CliApplication(commandTypes);
// Act
- var exitCodeValue = await application.RunAsync();
+ var exitCodeValue = await application.RunAsync(commandLineArguments);
// Assert
- Assert.That(exitCodeValue, Is.EqualTo(TestCommand.ExitCode.Value), "Exit code");
+ Assert.That(exitCodeValue, Is.Zero, "Exit code");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
+ public async Task RunAsync_Negative_Test(IReadOnlyList commandTypes, IReadOnlyList commandLineArguments)
+ {
+ // Arrange
+ var application = new CliApplication(commandTypes);
+
+ // Act
+ var exitCodeValue = await application.RunAsync(commandLineArguments);
+
+ // Assert
+ Assert.That(exitCodeValue, Is.Not.Zero, "Exit code");
}
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj
index 11b59d6..811a4d4 100644
--- a/CliFx.Tests/CliFx.Tests.csproj
+++ b/CliFx.Tests/CliFx.Tests.csproj
@@ -1,7 +1,7 @@
- net45
+ net46
false
true
true
diff --git a/CliFx.Tests/CommandFactoryTests.cs b/CliFx.Tests/CommandFactoryTests.cs
new file mode 100644
index 0000000..ada7fdc
--- /dev/null
+++ b/CliFx.Tests/CommandFactoryTests.cs
@@ -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 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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/CommandInitializerTests.cs b/CliFx.Tests/CommandInitializerTests.cs
index 83213a8..de61315 100644
--- a/CliFx.Tests/CommandInitializerTests.cs
+++ b/CliFx.Tests/CommandInitializerTests.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
@@ -10,7 +11,6 @@ namespace CliFx.Tests
{
public partial class CommandInitializerTests
{
- [Command]
public class TestCommand : ICommand
{
[CommandOption("int", 'i', IsRequired = true)]
@@ -22,9 +22,7 @@ namespace CliFx.Tests
[CommandOption("bool", 'b', GroupName = "other-group")]
public bool BoolOption { get; set; }
- public CommandContext Context { get; set; }
-
- public Task ExecuteAsync() => throw new System.NotImplementedException();
+ public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
}
}
@@ -94,26 +92,6 @@ namespace CliFx.Tests
);
}
- [Test]
- [TestCaseSource(nameof(GetTestCases_InitializeCommand))]
- public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
- {
- // Arrange
- var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
-
- // Act
- var command = initializer.InitializeCommand(commandInput) as TestCommand;
-
- // Assert
- Assert.Multiple(() =>
- {
- Assert.That(command, Is.Not.Null);
- Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
- Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
- Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
- });
- }
-
private static IEnumerable GetTestCases_InitializeCommand_IsRequired()
{
yield return new TestCaseData(CommandInput.Empty);
@@ -126,15 +104,38 @@ namespace CliFx.Tests
);
}
+ [Test]
+ [TestCaseSource(nameof(GetTestCases_InitializeCommand))]
+ public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
+ {
+ // Arrange
+ var initializer = new CommandInitializer();
+
+ // Act
+ var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
+ var command = new TestCommand();
+ initializer.InitializeCommand(command, schema, commandInput);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
+ Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
+ Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
+ });
+ }
+
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))]
public void InitializeCommand_IsRequired_Test(CommandInput commandInput)
{
// Arrange
- var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
+ var initializer = new CommandInitializer();
// Act & Assert
- Assert.Throws(() => initializer.InitializeCommand(commandInput));
+ var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
+ var command = new TestCommand();
+ Assert.Throws(() => initializer.InitializeCommand(command, schema, commandInput));
}
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/CommandSchemaResolverTests.cs b/CliFx.Tests/CommandSchemaResolverTests.cs
index 2f2ab36..c519f16 100644
--- a/CliFx.Tests/CommandSchemaResolverTests.cs
+++ b/CliFx.Tests/CommandSchemaResolverTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Models;
@@ -10,8 +11,10 @@ namespace CliFx.Tests
{
public partial class CommandSchemaResolverTests
{
- [Command(Description = "Command description")]
- public class TestCommand : ICommand
+ [Command("Command name", Description = "Command description")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
+ private class TestCommand : ICommand
{
[CommandOption("option-a", 'a', GroupName = "Group 1")]
public int OptionA { get; set; }
@@ -22,9 +25,7 @@ namespace CliFx.Tests
[CommandOption("option-c", Description = "Option C description")]
public bool OptionC { get; set; }
- public CommandContext Context { get; set; }
-
- public Task ExecuteAsync() => throw new NotImplementedException();
+ public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
}
}
@@ -34,36 +35,32 @@ namespace CliFx.Tests
private static IEnumerable GetTestCases_ResolveAllSchemas()
{
yield return new TestCaseData(
- new[] {typeof(TestCommand)},
- new[]
- {
- new CommandSchema(typeof(TestCommand),
- null, true, "Command description",
- new[]
- {
- new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)),
- "option-a", 'a', false, "Group 1", null),
- new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionB)),
- "option-b", null, true, null, null),
- new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionC)),
- "option-c", null, false, null, "Option C description")
- })
- }
+ typeof(TestCommand),
+ new CommandSchema(typeof(TestCommand), "Command name", "Command description",
+ new[]
+ {
+ new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)),
+ "option-a", 'a', "Group 1", false, null),
+ new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionB)),
+ "option-b", null, null, true, null),
+ new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionC)),
+ "option-c", null, null, false, "Option C description")
+ })
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))]
- public void ResolveAllSchemas_Test(IReadOnlyList sourceTypes, IReadOnlyList expectedSchemas)
+ public void GetCommandSchema_Test(Type commandType, CommandSchema expectedSchema)
{
// Arrange
- var resolver = new CommandSchemaResolver(sourceTypes);
+ var resolver = new CommandSchemaResolver();
// Act
- var schemas = resolver.ResolveAllSchemas();
+ var schema = resolver.GetCommandSchema(commandType);
// Assert
- Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance));
+ Assert.That(schema, Is.EqualTo(expectedSchema).Using(CommandSchemaEqualityComparer.Instance));
}
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs
index 7b334c1..fc18830 100644
--- a/CliFx.Tests/DummyTests.cs
+++ b/CliFx.Tests/DummyTests.cs
@@ -14,61 +14,59 @@ namespace CliFx.Tests
[Test]
[TestCase("", "Hello world")]
[TestCase("-t .NET", "Hello .NET")]
- [TestCase("-e", "Hello world!!!")]
- [TestCase("add -v 1 2", "3")]
- [TestCase("add -v 2.75 3.6 4.18", "10.53")]
- [TestCase("add -v 4 -v 16", "20")]
+ [TestCase("-e", "Hello world!")]
+ [TestCase("sum -v 1 2", "3")]
+ [TestCase("sum -v 2.75 3.6 4.18", "10.53")]
+ [TestCase("sum -v 4 -v 16", "20")]
+ [TestCase("sum --values 2 5 --values 3", "10")]
[TestCase("log -v 100", "2")]
[TestCase("log --value 256 --base 2", "8")]
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
{
- // Act
- var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
+ // Arrange & Act
+ var result = await Cli.Wrap(DummyFilePath)
+ .SetArguments(arguments)
+ .EnableExitCodeValidation()
+ .EnableStandardErrorValidation()
+ .ExecuteAsync();
// Assert
- Assert.Multiple(() =>
- {
- Assert.That(result.ExitCode, Is.Zero, "Exit code");
- Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
- Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
- });
+ Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout");
}
[Test]
[TestCase("--version")]
- public async Task CliApplication_RunAsync_Version_Test(string arguments)
+ public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments)
{
- // Act
- var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
+ // Arrange & Act
+ var result = await Cli.Wrap(DummyFilePath)
+ .SetArguments(arguments)
+ .EnableExitCodeValidation()
+ .EnableStandardErrorValidation()
+ .ExecuteAsync();
// Assert
- Assert.Multiple(() =>
- {
- Assert.That(result.ExitCode, Is.Zero, "Exit code");
- Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout");
- Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
- });
+ Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout");
}
[Test]
[TestCase("--help")]
[TestCase("-h")]
- [TestCase("add -h")]
- [TestCase("add --help")]
+ [TestCase("sum -h")]
+ [TestCase("sum --help")]
[TestCase("log -h")]
[TestCase("log --help")]
- public async Task CliApplication_RunAsync_Help_Test(string arguments)
+ public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments)
{
- // Act
- var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
+ // Arrange & Act
+ var result = await Cli.Wrap(DummyFilePath)
+ .SetArguments(arguments)
+ .EnableExitCodeValidation()
+ .EnableStandardErrorValidation()
+ .ExecuteAsync();
// Assert
- Assert.Multiple(() =>
- {
- Assert.That(result.ExitCode, Is.Zero, "Exit code");
- Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout");
- Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr");
- });
+ Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout");
}
}
}
\ No newline at end of file
diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs
index 5c88b58..ce77a7b 100644
--- a/CliFx/Attributes/CommandAttribute.cs
+++ b/CliFx/Attributes/CommandAttribute.cs
@@ -10,8 +10,6 @@ namespace CliFx.Attributes
public string Description { get; set; }
- public bool IsDefault => Name.IsNullOrWhiteSpace();
-
public CommandAttribute(string name)
{
Name = name;
diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs
index 36eea04..dc09f5e 100644
--- a/CliFx/Attributes/CommandOptionAttribute.cs
+++ b/CliFx/Attributes/CommandOptionAttribute.cs
@@ -9,10 +9,10 @@ namespace CliFx.Attributes
public char? ShortName { get; }
- public bool IsRequired { get; set; }
-
public string GroupName { get; set; }
+ public bool IsRequired { get; set; }
+
public string Description { get; set; }
public CommandOptionAttribute(string name, char? shortName)
diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs
index 4fd04e4..4893442 100644
--- a/CliFx/CliApplication.cs
+++ b/CliFx/CliApplication.cs
@@ -1,33 +1,137 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
+using CliFx.Exceptions;
+using CliFx.Internal;
+using CliFx.Models;
using CliFx.Services;
namespace CliFx
{
- public class CliApplication : ICliApplication
+ public partial class CliApplication : ICliApplication
{
+ private readonly IReadOnlyList _commandTypes;
private readonly ICommandInputParser _commandInputParser;
+ private readonly ICommandSchemaResolver _commandSchemaResolver;
+ private readonly ICommandFactory _commandFactory;
private readonly ICommandInitializer _commandInitializer;
+ private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
- public CliApplication(ICommandInputParser commandInputParser, ICommandInitializer commandInitializer)
+ public CliApplication(IReadOnlyList commandTypes,
+ ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
+ ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextBuilder commandHelpTextBuilder)
{
+ _commandTypes = commandTypes;
_commandInputParser = commandInputParser;
+ _commandSchemaResolver = commandSchemaResolver;
+ _commandFactory = commandFactory;
_commandInitializer = commandInitializer;
+ _commandHelpTextBuilder = commandHelpTextBuilder;
+ }
+
+ public CliApplication(IReadOnlyList commandTypes)
+ : this(commandTypes,
+ new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(),
+ new CommandInitializer(), new CommandHelpTextBuilder())
+ {
}
public CliApplication()
- : this(new CommandInputParser(), new CommandInitializer())
+ : this(GetDefaultCommandTypes())
{
}
public async Task RunAsync(IReadOnlyList commandLineArguments)
{
- var input = _commandInputParser.ParseInput(commandLineArguments);
- var command = _commandInitializer.InitializeCommand(input);
+ var stdOut = ConsoleWriter.GetStandardOutput();
+ var stdErr = ConsoleWriter.GetStandardError();
- var exitCode = await command.ExecuteAsync();
+ try
+ {
+ var commandInput = _commandInputParser.ParseInput(commandLineArguments);
- return exitCode.Value;
+ var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_commandTypes);
+ var matchingCommandSchema = availableCommandSchemas.FindByNameOrNull(commandInput.CommandName);
+
+ // Fail if specified a command which is not defined
+ if (commandInput.IsCommandSpecified() && matchingCommandSchema == null)
+ {
+ stdErr.WriteLine($"Specified command [{commandInput.CommandName}] doesn't exist.");
+ return -1;
+ }
+
+ // Show version if it was requested without specifying a command
+ if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
+ {
+ var versionText = Assembly.GetEntryAssembly()?.GetName().Version.ToString();
+ stdOut.WriteLine(versionText);
+ return 0;
+ }
+
+ // Use a stub if command was not specified but there is no default command defined
+ if (matchingCommandSchema == null)
+ {
+ matchingCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand));
+ }
+
+ // Show help if it was requested
+ if (commandInput.IsHelpRequested())
+ {
+ var helpText = _commandHelpTextBuilder.Build(availableCommandSchemas, matchingCommandSchema);
+ stdOut.WriteLine(helpText);
+ return 0;
+ }
+
+ // Create an instance of the command
+ var command = matchingCommandSchema.Type == typeof(StubDefaultCommand)
+ ? new StubDefaultCommand(_commandHelpTextBuilder)
+ : _commandFactory.CreateCommand(matchingCommandSchema);
+
+ // Populate command with options according to its schema
+ _commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput);
+
+ // Create context and execute command
+ var commandContext = new CommandContext(commandInput, availableCommandSchemas, matchingCommandSchema, stdOut, stdErr);
+ await command.ExecuteAsync(commandContext);
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ stdErr.WriteLine(ex.ToString());
+ return ex is CommandErrorException errorException ? errorException.ExitCode : -1;
+ }
+ finally
+ {
+ stdOut.Dispose();
+ stdErr.Dispose();
+ }
+ }
+ }
+
+ public partial class CliApplication
+ {
+ private static IReadOnlyList 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;
+ }
}
}
}
\ No newline at end of file
diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj
index deae8ad..1fc7a2e 100644
--- a/CliFx/CliFx.csproj
+++ b/CliFx/CliFx.csproj
@@ -1,7 +1,7 @@
- net45;netstandard2.0
+ net46;netstandard2.0
latest
0.0.1
Tyrrrz
diff --git a/CliFx/Command.cs b/CliFx/Command.cs
deleted file mode 100644
index 4eedc9a..0000000
--- a/CliFx/Command.cs
+++ /dev/null
@@ -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 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 ExecuteAsync()
- {
- if (IsHelpRequested)
- {
- ShowHelp();
- return Task.FromResult(ExitCode.Success);
- }
-
- if (IsVersionRequested && Context.CommandSchema.IsDefault)
- {
- ShowVersion();
- return Task.FromResult(ExitCode.Success);
- }
-
- return ProcessAsync();
- }
- }
-}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CannotConvertCommandOptionException.cs b/CliFx/Exceptions/CannotConvertCommandOptionException.cs
new file mode 100644
index 0000000..3b76cd7
--- /dev/null
+++ b/CliFx/Exceptions/CannotConvertCommandOptionException.cs
@@ -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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CommandErrorException.cs b/CliFx/Exceptions/CommandErrorException.cs
new file mode 100644
index 0000000..9fb89a6
--- /dev/null
+++ b/CliFx/Exceptions/CommandErrorException.cs
@@ -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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CommandOptionConvertException.cs b/CliFx/Exceptions/CommandOptionConvertException.cs
deleted file mode 100644
index cf4a0df..0000000
--- a/CliFx/Exceptions/CommandOptionConvertException.cs
+++ /dev/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)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CommandResolveException.cs b/CliFx/Exceptions/CommandResolveException.cs
deleted file mode 100644
index 86684f1..0000000
--- a/CliFx/Exceptions/CommandResolveException.cs
+++ /dev/null
@@ -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)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/CliFx/Exceptions/MissingCommandOptionException.cs b/CliFx/Exceptions/MissingCommandOptionException.cs
new file mode 100644
index 0000000..e5c5a50
--- /dev/null
+++ b/CliFx/Exceptions/MissingCommandOptionException.cs
@@ -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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Extensions.cs b/CliFx/Extensions.cs
deleted file mode 100644
index 0ca26fc..0000000
--- a/CliFx/Extensions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Threading.Tasks;
-
-namespace CliFx
-{
- public static class Extensions
- {
- public static Task RunAsync(this ICliApplication application) => application.RunAsync(new string[0]);
- }
-}
\ No newline at end of file
diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs
index 7e21993..226826e 100644
--- a/CliFx/ICommand.cs
+++ b/CliFx/ICommand.cs
@@ -5,8 +5,6 @@ namespace CliFx
{
public interface ICommand
{
- CommandContext Context { get; set; }
-
- Task ExecuteAsync();
+ Task ExecuteAsync(CommandContext context);
}
}
\ No newline at end of file
diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs
index 24afa0d..2a1ddbc 100644
--- a/CliFx/Internal/Extensions.cs
+++ b/CliFx/Internal/Extensions.cs
@@ -11,49 +11,12 @@ namespace CliFx.Internal
public static string AsString(this char c) => new string(c, 1);
- public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
- {
- var index = s.IndexOf(sub, comparison);
- return index < 0 ? s : s.Substring(0, index);
- }
-
- public static string SubstringAfter(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
- {
- var index = s.IndexOf(sub, comparison);
- return index < 0 ? string.Empty : s.Substring(index + sub.Length, s.Length - index - sub.Length);
- }
+ public static string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source);
public static TValue GetValueOrDefault(this IReadOnlyDictionary dic, TKey key) =>
dic.TryGetValue(key, out var result) ? result : default;
- public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
- {
- while (s.StartsWith(sub, comparison))
- s = s.Substring(sub.Length);
-
- return s;
- }
-
- public static string TrimEnd(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
- {
- while (s.EndsWith(sub, comparison))
- s = s.Substring(0, s.Length - sub.Length);
-
- return s;
- }
-
- public static string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source);
-
- public static bool IsDerivedFrom(this Type type, Type baseType)
- {
- for (var currentType = type; currentType != null; currentType = currentType.BaseType)
- {
- if (currentType == baseType)
- return true;
- }
-
- return false;
- }
+ public static IEnumerable ExceptNull(this IEnumerable source) where T : class => source.Where(i => i != null);
public static bool IsEnumerable(this Type type) =>
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
@@ -79,5 +42,7 @@ namespace CliFx.Internal
return array;
}
+
+ public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
}
}
\ No newline at end of file
diff --git a/CliFx/Models/CommandContext.cs b/CliFx/Models/CommandContext.cs
index 7f06661..ff02725 100644
--- a/CliFx/Models/CommandContext.cs
+++ b/CliFx/Models/CommandContext.cs
@@ -1,17 +1,29 @@
using System.Collections.Generic;
+using CliFx.Services;
namespace CliFx.Models
{
public class CommandContext
{
+ public CommandInput CommandInput { get; }
+
public IReadOnlyList AvailableCommandSchemas { get; }
- public CommandSchema CommandSchema { get; }
+ public CommandSchema MatchingCommandSchema { get; }
- public CommandContext(IReadOnlyList availableCommandSchemas, CommandSchema commandSchema)
+ public IConsoleWriter Output { get; }
+
+ public IConsoleWriter Error { get; }
+
+ public CommandContext(CommandInput commandInput,
+ IReadOnlyList availableCommandSchemas, CommandSchema matchingCommandSchema,
+ IConsoleWriter output, IConsoleWriter error)
{
+ CommandInput = commandInput;
AvailableCommandSchemas = availableCommandSchemas;
- CommandSchema = commandSchema;
+ MatchingCommandSchema = matchingCommandSchema;
+ Output = output;
+ Error = error;
}
}
}
\ No newline at end of file
diff --git a/CliFx/Models/CommandInput.cs b/CliFx/Models/CommandInput.cs
index 72ca70a..05caa58 100644
--- a/CliFx/Models/CommandInput.cs
+++ b/CliFx/Models/CommandInput.cs
@@ -50,7 +50,7 @@ namespace CliFx.Models
foreach (var option in Options)
{
- buffer.Append(option.Name);
+ buffer.Append(option.Alias);
}
buffer.Append(']');
diff --git a/CliFx/Models/CommandOptionInput.cs b/CliFx/Models/CommandOptionInput.cs
index 19f1646..94bc6c3 100644
--- a/CliFx/Models/CommandOptionInput.cs
+++ b/CliFx/Models/CommandOptionInput.cs
@@ -4,23 +4,23 @@ namespace CliFx.Models
{
public class CommandOptionInput
{
- public string Name { get; }
+ public string Alias { get; }
public IReadOnlyList Values { get; }
- public CommandOptionInput(string name, IReadOnlyList values)
+ public CommandOptionInput(string alias, IReadOnlyList values)
{
- Name = name;
+ Alias = alias;
Values = values;
}
- public CommandOptionInput(string name, string value)
- : this(name, new[] {value})
+ public CommandOptionInput(string alias, string value)
+ : this(alias, new[] {value})
{
}
- public CommandOptionInput(string name)
- : this(name, new string[0])
+ public CommandOptionInput(string alias)
+ : this(alias, new string[0])
{
}
}
diff --git a/CliFx/Models/CommandOptionInputEqualityComparer.cs b/CliFx/Models/CommandOptionInputEqualityComparer.cs
index 7b9d358..ae34f04 100644
--- a/CliFx/Models/CommandOptionInputEqualityComparer.cs
+++ b/CliFx/Models/CommandOptionInputEqualityComparer.cs
@@ -16,13 +16,13 @@ namespace CliFx.Models
if (x is null || y is null)
return false;
- return StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
+ return StringComparer.OrdinalIgnoreCase.Equals(x.Alias, y.Alias) &&
x.Values.SequenceEqual(y.Values, StringComparer.Ordinal);
}
///
public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder()
- .Add(obj.Name, StringComparer.OrdinalIgnoreCase)
+ .Add(obj.Alias, StringComparer.OrdinalIgnoreCase)
.AddMany(obj.Values, StringComparer.Ordinal)
.Build();
}
diff --git a/CliFx/Models/CommandOptionSchema.cs b/CliFx/Models/CommandOptionSchema.cs
index 6e5af5f..37be7d6 100644
--- a/CliFx/Models/CommandOptionSchema.cs
+++ b/CliFx/Models/CommandOptionSchema.cs
@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-using System.Reflection;
-using CliFx.Internal;
+using System.Reflection;
namespace CliFx.Models
{
@@ -12,14 +10,14 @@ namespace CliFx.Models
public char? ShortName { get; }
- public bool IsRequired { get; }
-
public string GroupName { get; }
+ public bool IsRequired { get; }
+
public string Description { get; }
public CommandOptionSchema(PropertyInfo property, string name, char? shortName,
- bool isRequired, string groupName, string description)
+ string groupName, bool isRequired, string description)
{
Property = property;
Name = name;
diff --git a/CliFx/Models/CommandSchema.cs b/CliFx/Models/CommandSchema.cs
index a4f8dea..e40deda 100644
--- a/CliFx/Models/CommandSchema.cs
+++ b/CliFx/Models/CommandSchema.cs
@@ -9,17 +9,14 @@ namespace CliFx.Models
public string Name { get; }
- public bool IsDefault { get; }
-
public string Description { get; }
public IReadOnlyList Options { get; }
- public CommandSchema(Type type, string name, bool isDefault, string description, IReadOnlyList options)
+ public CommandSchema(Type type, string name, string description, IReadOnlyList options)
{
Type = type;
Name = name;
- IsDefault = isDefault;
Description = description;
Options = options;
}
diff --git a/CliFx/Models/CommandSchemaEqualityComparer.cs b/CliFx/Models/CommandSchemaEqualityComparer.cs
index 0e7d809..943beb6 100644
--- a/CliFx/Models/CommandSchemaEqualityComparer.cs
+++ b/CliFx/Models/CommandSchemaEqualityComparer.cs
@@ -18,7 +18,6 @@ namespace CliFx.Models
return x.Type == y.Type &&
StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
- x.IsDefault == y.IsDefault &&
StringComparer.Ordinal.Equals(x.Description, y.Description) &&
x.Options.SequenceEqual(y.Options, CommandOptionSchemaEqualityComparer.Instance);
}
@@ -27,7 +26,6 @@ namespace CliFx.Models
public int GetHashCode(CommandSchema obj) => new HashCodeBuilder()
.Add(obj.Type)
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
- .Add(obj.IsDefault)
.Add(obj.Description, StringComparer.Ordinal)
.AddMany(obj.Options, CommandOptionSchemaEqualityComparer.Instance)
.Build();
diff --git a/CliFx/Models/ExitCode.cs b/CliFx/Models/ExitCode.cs
deleted file mode 100644
index d730edb..0000000
--- a/CliFx/Models/ExitCode.cs
+++ /dev/null
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/CliFx/Models/Extensions.cs b/CliFx/Models/Extensions.cs
index f12585f..6766d72 100644
--- a/CliFx/Models/Extensions.cs
+++ b/CliFx/Models/Extensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using CliFx.Internal;
@@ -6,16 +7,66 @@ namespace CliFx.Models
{
public static class Extensions
{
- public static CommandOptionInput GetOptionOrDefault(this CommandInput set, string name, char? shortName) =>
- set.Options.FirstOrDefault(o =>
- {
- if (!name.IsNullOrWhiteSpace() && string.Equals(o.Name, name, StringComparison.Ordinal))
- return true;
+ public static bool IsCommandSpecified(this CommandInput commandInput) => !commandInput.CommandName.IsNullOrWhiteSpace();
- if (shortName != null && o.Name.Length == 1 && o.Name.Single() == shortName)
- return true;
+ public static bool IsEmpty(this CommandInput commandInput) => !commandInput.IsCommandSpecified() && !commandInput.Options.Any();
- return false;
- });
+ 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 commandSchemas, string name) =>
+ commandSchemas.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ public static IReadOnlyList FindSubCommandSchemas(this IEnumerable commandSchemas,
+ string parentName)
+ {
+ // For a command with no name, every other command is its subcommand
+ if (parentName.IsNullOrWhiteSpace())
+ return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace()).ToArray();
+
+ // For a named command, commands that are prefixed by its name are its subcommands
+ return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace())
+ .Where(c => c.Name.StartsWith(parentName + " ", StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ }
+ public static CommandOptionSchema FindByAliasOrNull(this IEnumerable optionSchemas, string alias) =>
+ optionSchemas.FirstOrDefault(o => o.GetAliases().Contains(alias, StringComparer.OrdinalIgnoreCase));
+
+ public static IReadOnlyList GetAliases(this CommandOptionSchema optionSchema)
+ {
+ var result = new List();
+
+ if (!optionSchema.Name.IsNullOrWhiteSpace())
+ result.Add(optionSchema.Name);
+
+ if (optionSchema.ShortName != null)
+ result.Add(optionSchema.ShortName.Value.AsString());
+
+ return result;
+ }
+
+ public static IReadOnlyList GetAliasesWithPrefixes(this CommandOptionSchema optionSchema)
+ {
+ var result = new List();
+
+ if (!optionSchema.Name.IsNullOrWhiteSpace())
+ result.Add("--" + optionSchema.Name);
+
+ if (optionSchema.ShortName != null)
+ result.Add("-" + optionSchema.ShortName.Value.AsString());
+
+ return result;
+ }
}
}
\ No newline at end of file
diff --git a/CliFx/Services/CommandFactory.cs b/CliFx/Services/CommandFactory.cs
new file mode 100644
index 0000000..ff8db23
--- /dev/null
+++ b/CliFx/Services/CommandFactory.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/CommandHelpTextBuilder.cs b/CliFx/Services/CommandHelpTextBuilder.cs
new file mode 100644
index 0000000..8c035c6
--- /dev/null
+++ b/CliFx/Services/CommandHelpTextBuilder.cs
@@ -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 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 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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/CommandInitializer.cs b/CliFx/Services/CommandInitializer.cs
index 002f67e..6bf5c52 100644
--- a/CliFx/Services/CommandInitializer.cs
+++ b/CliFx/Services/CommandInitializer.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
@@ -10,115 +9,44 @@ namespace CliFx.Services
{
public class CommandInitializer : ICommandInitializer
{
- private readonly ITypeActivator _typeActivator;
- private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
- public CommandInitializer(ITypeActivator typeActivator, ICommandSchemaResolver commandSchemaResolver,
- ICommandOptionInputConverter commandOptionInputConverter)
+ public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter)
{
- _typeActivator = typeActivator;
- _commandSchemaResolver = commandSchemaResolver;
_commandOptionInputConverter = commandOptionInputConverter;
}
- public CommandInitializer(ICommandSchemaResolver commandSchemaResolver)
- : this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter())
- {
- }
-
public CommandInitializer()
- : this(new CommandSchemaResolver())
+ : this(new CommandOptionInputConverter())
{
}
- private CommandSchema GetDefaultSchema(IReadOnlyList 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 schemas, string name)
- {
- // Get command types with given name
- var matchingSchemas =
- schemas.Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray();
-
- // If there's only one type - return
- if (matchingSchemas.Length == 1)
- return matchingSchemas.Single();
-
- // If there are multiple - throw
- if (matchingSchemas.Length > 1)
- {
- throw new CommandResolveException(
- $"Can't resolve command because there is more than one command named [{name}]. " +
- "Make sure all command names are unique and keep in mind that comparison is case-insensitive.");
- }
-
- // If there aren't any - throw
- throw new CommandResolveException(
- $"Can't resolve command because none of the commands is named [{name}]. " +
- $"Apply {nameof(CommandAttribute)} to give command a name.");
- }
-
- // TODO: refactor
- public ICommand InitializeCommand(CommandInput input)
- {
- var schemas = _commandSchemaResolver.ResolveAllSchemas();
-
- // Get command type
- var schema = !input.CommandName.IsNullOrWhiteSpace()
- ? GetSchemaByName(schemas, input.CommandName)
- : GetDefaultSchema(schemas);
-
- // Activate command
- var command = (ICommand) _typeActivator.Activate(schema.Type);
- command.Context = new CommandContext(schemas, schema);
-
// Set command options
var isGroupNameDetected = false;
var groupName = default(string);
var properties = new HashSet();
foreach (var option in input.Options)
{
- var optionInfo = schema.Options.FirstOrDefault(p =>
- string.Equals(p.Name, option.Name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(p.ShortName?.AsString(), option.Name, StringComparison.OrdinalIgnoreCase));
+ var optionSchema = schema.Options.FindByAliasOrNull(option.Alias);
- if (optionInfo == null)
+ if (optionSchema == null)
continue;
- if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase))
+ if (isGroupNameDetected && !string.Equals(groupName, optionSchema.GroupName, StringComparison.OrdinalIgnoreCase))
continue;
if (!isGroupNameDetected)
{
- groupName = optionInfo.GroupName;
+ groupName = optionSchema.GroupName;
isGroupNameDetected = true;
}
- var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType);
- optionInfo.Property.SetValue(command, convertedValue);
+ var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionSchema.Property.PropertyType);
+ optionSchema.Property.SetValue(command, convertedValue);
- properties.Add(optionInfo);
+ properties.Add(optionSchema);
}
var unsetRequiredOptions = schema.Options
@@ -128,10 +56,8 @@ namespace CliFx.Services
.ToArray();
if (unsetRequiredOptions.Any())
- throw new CommandResolveException(
+ throw new MissingCommandOptionException(
$"Can't resolve command because one or more required properties were not set: {unsetRequiredOptions.Select(p => p.Name).JoinToString(", ")}");
-
- return command;
}
}
}
\ No newline at end of file
diff --git a/CliFx/Services/CommandOptionInputConverter.cs b/CliFx/Services/CommandOptionInputConverter.cs
index be8750b..75bafbc 100644
--- a/CliFx/Services/CommandOptionInputConverter.cs
+++ b/CliFx/Services/CommandOptionInputConverter.cs
@@ -39,7 +39,7 @@ namespace CliFx.Services
if (bool.TryParse(value, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to boolean.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to boolean.");
}
// Char
@@ -48,7 +48,7 @@ namespace CliFx.Services
if (value.Length == 1)
return value[0];
- throw new CommandOptionConvertException(
+ throw new CannotConvertCommandOptionException(
$"Can't convert value [{value}] to char. The value is either empty or longer than one character.");
}
@@ -58,7 +58,7 @@ namespace CliFx.Services
if (sbyte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to sbyte.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to sbyte.");
}
// Byte
@@ -67,7 +67,7 @@ namespace CliFx.Services
if (byte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to byte.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to byte.");
}
// Short
@@ -76,7 +76,7 @@ namespace CliFx.Services
if (short.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to short.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to short.");
}
// Ushort
@@ -85,7 +85,7 @@ namespace CliFx.Services
if (ushort.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to ushort.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ushort.");
}
// Int
@@ -94,7 +94,7 @@ namespace CliFx.Services
if (int.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to int.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to int.");
}
// Uint
@@ -103,7 +103,7 @@ namespace CliFx.Services
if (uint.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to uint.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to uint.");
}
// Long
@@ -112,7 +112,7 @@ namespace CliFx.Services
if (long.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to long.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to long.");
}
// Ulong
@@ -121,7 +121,7 @@ namespace CliFx.Services
if (ulong.TryParse(value, NumberStyles.Integer, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to ulong.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ulong.");
}
// Float
@@ -130,7 +130,7 @@ namespace CliFx.Services
if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to float.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to float.");
}
// Double
@@ -139,7 +139,7 @@ namespace CliFx.Services
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to double.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to double.");
}
// Decimal
@@ -148,7 +148,7 @@ namespace CliFx.Services
if (decimal.TryParse(value, NumberStyles.Number, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to decimal.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to decimal.");
}
// DateTime
@@ -157,7 +157,7 @@ namespace CliFx.Services
if (DateTime.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTime.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTime.");
}
// DateTimeOffset
@@ -166,7 +166,7 @@ namespace CliFx.Services
if (DateTimeOffset.TryParse(value, _formatProvider, DateTimeStyles.None, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTimeOffset.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTimeOffset.");
}
// TimeSpan
@@ -175,7 +175,7 @@ namespace CliFx.Services
if (TimeSpan.TryParse(value, _formatProvider, out var result))
return result;
- throw new CommandOptionConvertException($"Can't convert value [{value}] to TimeSpan.");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to TimeSpan.");
}
// Enum
@@ -184,7 +184,7 @@ namespace CliFx.Services
if (Enum.GetNames(targetType).Contains(value, StringComparer.OrdinalIgnoreCase))
return Enum.Parse(targetType, value, true);
- throw new CommandOptionConvertException(
+ throw new CannotConvertCommandOptionException(
$"Can't convert value [{value}] to [{targetType}]. The value is not defined on the enum.");
}
@@ -213,7 +213,7 @@ namespace CliFx.Services
}
// Unknown type
- throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}].");
+ throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to unrecognized type [{targetType}].");
}
// TODO: refactor this
@@ -226,7 +226,7 @@ namespace CliFx.Services
if (targetType.IsAssignableFrom(underlyingType.MakeArrayType()))
return option.Values.Select(v => ConvertValue(v, underlyingType)).ToArray().ToNonGenericArray(underlyingType);
- throw new CommandOptionConvertException(
+ throw new CannotConvertCommandOptionException(
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
}
else if (option.Values.Count <= 1)
@@ -239,7 +239,7 @@ namespace CliFx.Services
else
{
// TODO: better exception
- throw new CommandOptionConvertException(
+ throw new CannotConvertCommandOptionException(
$"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}].");
}
}
diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs
index a801035..d2b4d59 100644
--- a/CliFx/Services/CommandSchemaResolver.cs
+++ b/CliFx/Services/CommandSchemaResolver.cs
@@ -1,74 +1,42 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CliFx.Attributes;
+using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
public class CommandSchemaResolver : ICommandSchemaResolver
{
- private readonly IReadOnlyList _sourceTypes;
-
- public CommandSchemaResolver(IReadOnlyList sourceTypes)
+ private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
{
- _sourceTypes = sourceTypes;
+ var attribute = optionProperty.GetCustomAttribute();
+
+ if (attribute == null)
+ return null;
+
+ return new CommandOptionSchema(optionProperty,
+ attribute.Name,
+ attribute.ShortName,
+ attribute.GroupName,
+ attribute.IsRequired, attribute.Description);
}
- public CommandSchemaResolver(IReadOnlyList sourceAssemblies)
- : this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray())
+ // TODO: validate stuff like duplicate names, multiple default commands, etc
+ public CommandSchema GetCommandSchema(Type commandType)
{
- }
+ if (!commandType.Implements(typeof(ICommand)))
+ throw new ArgumentException($"Command type must implement {nameof(ICommand)}.", nameof(commandType));
- public CommandSchemaResolver()
- : this(new[] {Assembly.GetEntryAssembly()})
- {
- }
+ var attribute = commandType.GetCustomAttribute();
- private IEnumerable GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand)));
+ var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
- private IReadOnlyList GetCommandOptionSchemas(Type commandType)
- {
- var result = new List();
-
- foreach (var optionProperty in commandType.GetProperties())
- {
- var optionAttribute = optionProperty.GetCustomAttribute();
-
- if (optionAttribute == null)
- continue;
-
- result.Add(new CommandOptionSchema(optionProperty,
- optionAttribute.Name,
- optionAttribute.ShortName,
- optionAttribute.IsRequired,
- optionAttribute.GroupName,
- optionAttribute.Description));
- }
-
- return result;
- }
-
- public IReadOnlyList ResolveAllSchemas()
- {
- var result = new List();
-
- foreach (var commandType in GetCommandTypes())
- {
- var commandAttribute = commandType.GetCustomAttribute();
-
- if (commandAttribute == null)
- continue;
-
- result.Add(new CommandSchema(commandType,
- commandAttribute.Name,
- commandAttribute.IsDefault,
- commandAttribute.Description,
- GetCommandOptionSchemas(commandType)));
- }
-
- return result;
+ return new CommandSchema(commandType,
+ attribute?.Name,
+ attribute?.Description,
+ options);
}
}
}
\ No newline at end of file
diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs
index 8969486..6eb45df 100644
--- a/CliFx/Services/Extensions.cs
+++ b/CliFx/Services/Extensions.cs
@@ -1,11 +1,42 @@
-using CliFx.Models;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using CliFx.Models;
namespace CliFx.Services
{
public static class Extensions
{
- public static void Write(this IConsoleWriter consoleWriter, string text) => consoleWriter.Write(new TextSpan(text));
+ public static IReadOnlyList GetCommandSchemas(this ICommandSchemaResolver commandSchemaResolver,
+ IEnumerable 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());
+ }
}
}
\ No newline at end of file
diff --git a/CliFx/Services/HelpTextBuilder.cs b/CliFx/Services/HelpTextBuilder.cs
deleted file mode 100644
index 7fb5eb3..0000000
--- a/CliFx/Services/HelpTextBuilder.cs
+++ /dev/null
@@ -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 GetOptionIdentifiers(CommandOptionSchema option)
- {
- var result = new List();
-
- 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();
- }
- }
-}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandFactory.cs b/CliFx/Services/ICommandFactory.cs
new file mode 100644
index 0000000..313bde9
--- /dev/null
+++ b/CliFx/Services/ICommandFactory.cs
@@ -0,0 +1,10 @@
+using System;
+using CliFx.Models;
+
+namespace CliFx.Services
+{
+ public interface ICommandFactory
+ {
+ ICommand CreateCommand(CommandSchema schema);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandHelpTextBuilder.cs b/CliFx/Services/ICommandHelpTextBuilder.cs
new file mode 100644
index 0000000..c789bd7
--- /dev/null
+++ b/CliFx/Services/ICommandHelpTextBuilder.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using CliFx.Models;
+
+namespace CliFx.Services
+{
+ public interface ICommandHelpTextBuilder
+ {
+ string Build(IReadOnlyList commandSchemas, CommandSchema commandSchema);
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandInitializer.cs b/CliFx/Services/ICommandInitializer.cs
index ed26b71..07ee0cc 100644
--- a/CliFx/Services/ICommandInitializer.cs
+++ b/CliFx/Services/ICommandInitializer.cs
@@ -4,6 +4,6 @@ namespace CliFx.Services
{
public interface ICommandInitializer
{
- ICommand InitializeCommand(CommandInput input);
+ void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input);
}
}
\ No newline at end of file
diff --git a/CliFx/Services/ICommandSchemaResolver.cs b/CliFx/Services/ICommandSchemaResolver.cs
index 576383b..ed82aad 100644
--- a/CliFx/Services/ICommandSchemaResolver.cs
+++ b/CliFx/Services/ICommandSchemaResolver.cs
@@ -1,10 +1,10 @@
-using System.Collections.Generic;
+using System;
using CliFx.Models;
namespace CliFx.Services
{
public interface ICommandSchemaResolver
{
- IReadOnlyList ResolveAllSchemas();
+ CommandSchema GetCommandSchema(Type commandType);
}
}
\ No newline at end of file
diff --git a/CliFx/Services/IHelpTextBuilder.cs b/CliFx/Services/IHelpTextBuilder.cs
deleted file mode 100644
index 4190981..0000000
--- a/CliFx/Services/IHelpTextBuilder.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using CliFx.Models;
-
-namespace CliFx.Services
-{
- public interface IHelpTextBuilder
- {
- string Build(CommandContext context);
- }
-}
\ No newline at end of file
diff --git a/CliFx/Services/ITypeActivator.cs b/CliFx/Services/ITypeActivator.cs
deleted file mode 100644
index 30236c5..0000000
--- a/CliFx/Services/ITypeActivator.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace CliFx.Services
-{
- public interface ITypeActivator
- {
- object Activate(Type type);
- }
-}
\ No newline at end of file
diff --git a/CliFx/Services/TypeActivator.cs b/CliFx/Services/TypeActivator.cs
deleted file mode 100644
index c2f1bf3..0000000
--- a/CliFx/Services/TypeActivator.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace CliFx.Services
-{
- public class TypeActivator : ITypeActivator
- {
- public object Activate(Type type) => Activator.CreateInstance(type);
- }
-}
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
index 0a1d91b..1ce1e26 100644
--- a/Readme.md
+++ b/Readme.md
@@ -18,7 +18,7 @@ CliFx is a powerful framework for building command line applications.
## Features
- ...to be added with a stable release...
-- Targets .NET Framework 4.5+ and .NET Standard 2.0+
+- Targets .NET Framework 4.6+ and .NET Standard 2.0+
- No external dependencies
## Usage