Rework architecture again

This commit is contained in:
Alexey Golub
2019-07-25 01:14:49 +03:00
parent 2bdb2bddc8
commit d2599af90b
48 changed files with 880 additions and 664 deletions

View File

@@ -2,8 +2,9 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net45</TargetFramework>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<Version>1.2.3.4</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -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;
}
}
}

View 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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,24 +1,24 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Models;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command("add", Description = "Calculate the sum of all input values.")]
public class AddCommand : Command
[Command("sum", Description = "Calculates the sum of all input values.")]
public class SumCommand : ICommand
{
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
public IReadOnlyList<double> Values { get; set; }
protected override ExitCode Process()
public Task ExecuteAsync(CommandContext context)
{
var result = Values.Sum();
Output.WriteLine(result.ToString(CultureInfo.InvariantCulture));
context.Output.WriteLine(result);
return ExitCode.Success;
return Task.CompletedTask;
}
}
}

View File

@@ -1,7 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using NUnit.Framework;
namespace CliFx.Tests
@@ -9,32 +11,187 @@ namespace CliFx.Tests
public partial class CliApplicationTests
{
[Command]
public class TestCommand : ICommand
private class TestDefaultCommand : ICommand
{
public static ExitCode ExitCode { get; } = new ExitCode(13);
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
}
public CommandContext Context { get; set; }
[Command("command")]
private class TestNamedCommand : ICommand
{
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
}
public Task<ExitCode> ExecuteAsync() => Task.FromResult(ExitCode);
[Command("faulty-command")]
private class FaultyCommand : ICommand
{
public Task ExecuteAsync(CommandContext context) => Task.FromException(new CommandErrorException(-1337));
}
}
[TestFixture]
public partial class CliApplicationTests
{
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
// Specified command is defined
yield return new TestCaseData(
new[] {typeof(TestNamedCommand)},
new[] {"command"}
);
yield return new TestCaseData(
new[] {typeof(TestNamedCommand)},
new[] {"command", "--help"}
);
yield return new TestCaseData(
new[] {typeof(TestNamedCommand)},
new[] {"command", "-h"}
);
yield return new TestCaseData(
new[] {typeof(TestNamedCommand)},
new[] {"command", "-?"}
);
// Default command is defined
yield return new TestCaseData(
new[] {typeof(TestDefaultCommand)},
new string[0]
);
yield return new TestCaseData(
new[] {typeof(TestDefaultCommand)},
new[] {"--version"}
);
yield return new TestCaseData(
new[] {typeof(TestDefaultCommand)},
new[] {"--help"}
);
yield return new TestCaseData(
new[] {typeof(TestDefaultCommand)},
new[] {"-h"}
);
yield return new TestCaseData(
new[] {typeof(TestDefaultCommand)},
new[] {"-?"}
);
// Default command is not defined
yield return new TestCaseData(
new Type[0],
new string[0]
);
yield return new TestCaseData(
new Type[0],
new[] {"--version"}
);
yield return new TestCaseData(
new Type[0],
new[] {"--help"}
);
yield return new TestCaseData(
new Type[0],
new[] {"-h"}
);
yield return new TestCaseData(
new Type[0],
new[] {"-?"}
);
// Specified a faulty command
yield return new TestCaseData(
new[] {typeof(FaultyCommand)},
new[] {"--version"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand)},
new[] {"--help"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand)},
new[] {"-h"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand)},
new[] {"-?"}
);
}
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
{
// Specified command is not defined
yield return new TestCaseData(
new Type[0],
new[] {"command"}
);
yield return new TestCaseData(
new Type[0],
new[] {"command", "--help"}
);
yield return new TestCaseData(
new Type[0],
new[] {"command", "-h"}
);
yield return new TestCaseData(
new Type[0],
new[] {"command", "-?"}
);
// Specified a faulty command
yield return new TestCaseData(
new[] {typeof(FaultyCommand)},
new[] {"faulty-command"}
);
}
[Test]
public async Task RunAsync_Test()
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
{
// Arrange
var application = new CliApplication(
new CommandInputParser(),
new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})));
var application = new CliApplication(commandTypes);
// Act
var exitCodeValue = await application.RunAsync();
var exitCodeValue = await application.RunAsync(commandLineArguments);
// Assert
Assert.That(exitCodeValue, Is.EqualTo(TestCommand.ExitCode.Value), "Exit code");
Assert.That(exitCodeValue, Is.Zero, "Exit code");
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
{
// Arrange
var application = new CliApplication(commandTypes);
// Act
var exitCodeValue = await application.RunAsync(commandLineArguments);
// Assert
Assert.That(exitCodeValue, Is.Not.Zero, "Exit code");
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net45</TargetFramework>
<TargetFramework>net46</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>

View 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));
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
@@ -10,7 +11,6 @@ namespace CliFx.Tests
{
public partial class CommandInitializerTests
{
[Command]
public class TestCommand : ICommand
{
[CommandOption("int", 'i', IsRequired = true)]
@@ -22,9 +22,7 @@ namespace CliFx.Tests
[CommandOption("bool", 'b', GroupName = "other-group")]
public bool BoolOption { get; set; }
public CommandContext Context { get; set; }
public Task<ExitCode> ExecuteAsync() => throw new System.NotImplementedException();
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
}
}
@@ -94,26 +92,6 @@ namespace CliFx.Tests
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
{
// Arrange
var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
// Act
var command = initializer.InitializeCommand(commandInput) as TestCommand;
// Assert
Assert.Multiple(() =>
{
Assert.That(command, Is.Not.Null);
Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
});
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_IsRequired()
{
yield return new TestCaseData(CommandInput.Empty);
@@ -126,15 +104,38 @@ namespace CliFx.Tests
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand)
{
// Arrange
var initializer = new CommandInitializer();
// Act
var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
var command = new TestCommand();
initializer.InitializeCommand(command, schema, commandInput);
// Assert
Assert.Multiple(() =>
{
Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption));
});
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))]
public void InitializeCommand_IsRequired_Test(CommandInput commandInput)
{
// Arrange
var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}));
var initializer = new CommandInitializer();
// Act & Assert
Assert.Throws<CommandResolveException>(() => initializer.InitializeCommand(commandInput));
var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand));
var command = new TestCommand();
Assert.Throws<MissingCommandOptionException>(() => initializer.InitializeCommand(command, schema, commandInput));
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Models;
@@ -10,8 +11,10 @@ namespace CliFx.Tests
{
public partial class CommandSchemaResolverTests
{
[Command(Description = "Command description")]
public class TestCommand : ICommand
[Command("Command name", Description = "Command description")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
private class TestCommand : ICommand
{
[CommandOption("option-a", 'a', GroupName = "Group 1")]
public int OptionA { get; set; }
@@ -22,9 +25,7 @@ namespace CliFx.Tests
[CommandOption("option-c", Description = "Option C description")]
public bool OptionC { get; set; }
public CommandContext Context { get; set; }
public Task<ExitCode> ExecuteAsync() => throw new NotImplementedException();
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
}
}
@@ -34,36 +35,32 @@ namespace CliFx.Tests
private static IEnumerable<TestCaseData> GetTestCases_ResolveAllSchemas()
{
yield return new TestCaseData(
new[] {typeof(TestCommand)},
new[]
{
new CommandSchema(typeof(TestCommand),
null, true, "Command description",
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<Type> sourceTypes, IReadOnlyList<CommandSchema> expectedSchemas)
public void GetCommandSchema_Test(Type commandType, CommandSchema expectedSchema)
{
// Arrange
var resolver = new CommandSchemaResolver(sourceTypes);
var resolver = new CommandSchemaResolver();
// Act
var schemas = resolver.ResolveAllSchemas();
var schema = resolver.GetCommandSchema(commandType);
// Assert
Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance));
Assert.That(schema, Is.EqualTo(expectedSchema).Using(CommandSchemaEqualityComparer.Instance));
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -10,8 +10,6 @@ namespace CliFx.Attributes
public string Description { get; set; }
public bool IsDefault => Name.IsNullOrWhiteSpace();
public CommandAttribute(string name)
{
Name = name;

View File

@@ -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)

View File

@@ -1,33 +1,137 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx
{
public class CliApplication : ICliApplication
public partial class CliApplication : ICliApplication
{
private readonly IReadOnlyList<Type> _commandTypes;
private readonly ICommandInputParser _commandInputParser;
private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandFactory _commandFactory;
private readonly ICommandInitializer _commandInitializer;
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
public CliApplication(ICommandInputParser commandInputParser, ICommandInitializer commandInitializer)
public CliApplication(IReadOnlyList<Type> commandTypes,
ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextBuilder commandHelpTextBuilder)
{
_commandTypes = commandTypes;
_commandInputParser = commandInputParser;
_commandSchemaResolver = commandSchemaResolver;
_commandFactory = commandFactory;
_commandInitializer = commandInitializer;
_commandHelpTextBuilder = commandHelpTextBuilder;
}
public CliApplication(IReadOnlyList<Type> commandTypes)
: this(commandTypes,
new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(),
new CommandInitializer(), new CommandHelpTextBuilder())
{
}
public CliApplication()
: this(new CommandInputParser(), new CommandInitializer())
: this(GetDefaultCommandTypes())
{
}
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{
var input = _commandInputParser.ParseInput(commandLineArguments);
var command = _commandInitializer.InitializeCommand(input);
var stdOut = ConsoleWriter.GetStandardOutput();
var stdErr = ConsoleWriter.GetStandardError();
var exitCode = await command.ExecuteAsync();
try
{
var commandInput = _commandInputParser.ParseInput(commandLineArguments);
return exitCode.Value;
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_commandTypes);
var matchingCommandSchema = availableCommandSchemas.FindByNameOrNull(commandInput.CommandName);
// Fail if specified a command which is not defined
if (commandInput.IsCommandSpecified() && matchingCommandSchema == null)
{
stdErr.WriteLine($"Specified command [{commandInput.CommandName}] doesn't exist.");
return -1;
}
// Show version if it was requested without specifying a command
if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
{
var versionText = Assembly.GetEntryAssembly()?.GetName().Version.ToString();
stdOut.WriteLine(versionText);
return 0;
}
// Use a stub if command was not specified but there is no default command defined
if (matchingCommandSchema == null)
{
matchingCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand));
}
// Show help if it was requested
if (commandInput.IsHelpRequested())
{
var helpText = _commandHelpTextBuilder.Build(availableCommandSchemas, matchingCommandSchema);
stdOut.WriteLine(helpText);
return 0;
}
// Create an instance of the command
var command = matchingCommandSchema.Type == typeof(StubDefaultCommand)
? new StubDefaultCommand(_commandHelpTextBuilder)
: _commandFactory.CreateCommand(matchingCommandSchema);
// Populate command with options according to its schema
_commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput);
// Create context and execute command
var commandContext = new CommandContext(commandInput, availableCommandSchemas, matchingCommandSchema, stdOut, stdErr);
await command.ExecuteAsync(commandContext);
return 0;
}
catch (Exception ex)
{
stdErr.WriteLine(ex.ToString());
return ex is CommandErrorException errorException ? errorException.ExitCode : -1;
}
finally
{
stdOut.Dispose();
stdErr.Dispose();
}
}
}
public partial class CliApplication
{
private static IReadOnlyList<Type> GetDefaultCommandTypes() =>
Assembly.GetEntryAssembly()?.ExportedTypes.Where(t => t.Implements(typeof(ICommand))).ToArray() ??
Type.EmptyTypes;
private sealed class StubDefaultCommand : ICommand
{
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
public StubDefaultCommand(ICommandHelpTextBuilder commandHelpTextBuilder)
{
_commandHelpTextBuilder = commandHelpTextBuilder;
}
public Task ExecuteAsync(CommandContext context)
{
var helpText = _commandHelpTextBuilder.Build(context.AvailableCommandSchemas, context.MatchingCommandSchema);
context.Output.WriteLine(helpText);
return Task.CompletedTask;
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.0.1</Version>
<Company>Tyrrrz</Company>

View File

@@ -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();
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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)
{
}
}
}

View 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)
{
}
}
}

View File

@@ -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]);
}
}

View File

@@ -5,8 +5,6 @@ namespace CliFx
{
public interface ICommand
{
CommandContext Context { get; set; }
Task<ExitCode> ExecuteAsync();
Task ExecuteAsync(CommandContext context);
}
}

View File

@@ -11,49 +11,12 @@ namespace CliFx.Internal
public static string AsString(this char c) => new string(c, 1);
public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{
var index = s.IndexOf(sub, comparison);
return index < 0 ? s : s.Substring(0, index);
}
public static string SubstringAfter(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{
var index = s.IndexOf(sub, comparison);
return index < 0 ? string.Empty : s.Substring(index + sub.Length, s.Length - index - sub.Length);
}
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
dic.TryGetValue(key, out var result) ? result : default;
public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{
while (s.StartsWith(sub, comparison))
s = s.Substring(sub.Length);
return s;
}
public static string TrimEnd(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
{
while (s.EndsWith(sub, comparison))
s = s.Substring(0, s.Length - sub.Length);
return s;
}
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
public static bool IsDerivedFrom(this Type type, Type baseType)
{
for (var currentType = type; currentType != null; currentType = currentType.BaseType)
{
if (currentType == baseType)
return true;
}
return false;
}
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
public static bool IsEnumerable(this Type type) =>
type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable));
@@ -79,5 +42,7 @@ namespace CliFx.Internal
return array;
}
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
}
}

View File

@@ -1,17 +1,29 @@
using System.Collections.Generic;
using CliFx.Services;
namespace CliFx.Models
{
public class CommandContext
{
public CommandInput CommandInput { get; }
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
public CommandSchema CommandSchema { get; }
public CommandSchema MatchingCommandSchema { get; }
public CommandContext(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema commandSchema)
public IConsoleWriter Output { get; }
public IConsoleWriter Error { get; }
public CommandContext(CommandInput commandInput,
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema,
IConsoleWriter output, IConsoleWriter error)
{
CommandInput = commandInput;
AvailableCommandSchemas = availableCommandSchemas;
CommandSchema = commandSchema;
MatchingCommandSchema = matchingCommandSchema;
Output = output;
Error = error;
}
}
}

View File

@@ -50,7 +50,7 @@ namespace CliFx.Models
foreach (var option in Options)
{
buffer.Append(option.Name);
buffer.Append(option.Alias);
}
buffer.Append(']');

View File

@@ -4,23 +4,23 @@ namespace CliFx.Models
{
public class CommandOptionInput
{
public string Name { get; }
public string Alias { get; }
public IReadOnlyList<string> Values { get; }
public CommandOptionInput(string name, IReadOnlyList<string> values)
public CommandOptionInput(string alias, IReadOnlyList<string> values)
{
Name = name;
Alias = alias;
Values = values;
}
public CommandOptionInput(string name, string value)
: this(name, new[] {value})
public CommandOptionInput(string alias, string value)
: this(alias, new[] {value})
{
}
public CommandOptionInput(string name)
: this(name, new string[0])
public CommandOptionInput(string alias)
: this(alias, new string[0])
{
}
}

View File

@@ -16,13 +16,13 @@ namespace CliFx.Models
if (x is null || y is null)
return false;
return StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) &&
return StringComparer.OrdinalIgnoreCase.Equals(x.Alias, y.Alias) &&
x.Values.SequenceEqual(y.Values, StringComparer.Ordinal);
}
/// <inheritdoc />
public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder()
.Add(obj.Name, StringComparer.OrdinalIgnoreCase)
.Add(obj.Alias, StringComparer.OrdinalIgnoreCase)
.AddMany(obj.Values, StringComparer.Ordinal)
.Build();
}

View File

@@ -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;

View File

@@ -9,17 +9,14 @@ namespace CliFx.Models
public string Name { get; }
public bool IsDefault { get; }
public string Description { get; }
public IReadOnlyList<CommandOptionSchema> Options { get; }
public CommandSchema(Type type, string name, bool isDefault, string description, IReadOnlyList<CommandOptionSchema> options)
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandOptionSchema> options)
{
Type = type;
Name = name;
IsDefault = isDefault;
Description = description;
Options = options;
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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<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)
{
// 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<CommandOptionSchema> optionSchemas, string alias) =>
optionSchemas.FirstOrDefault(o => o.GetAliases().Contains(alias, StringComparer.OrdinalIgnoreCase));
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;
}
}
}

View 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);
}
}

View 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();
}
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
@@ -10,115 +9,44 @@ namespace CliFx.Services
{
public class CommandInitializer : ICommandInitializer
{
private readonly ITypeActivator _typeActivator;
private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
public CommandInitializer(ITypeActivator typeActivator, ICommandSchemaResolver commandSchemaResolver,
ICommandOptionInputConverter commandOptionInputConverter)
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter)
{
_typeActivator = typeActivator;
_commandSchemaResolver = commandSchemaResolver;
_commandOptionInputConverter = commandOptionInputConverter;
}
public CommandInitializer(ICommandSchemaResolver commandSchemaResolver)
: this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter())
{
}
public CommandInitializer()
: this(new CommandSchemaResolver())
: this(new CommandOptionInputConverter())
{
}
private CommandSchema GetDefaultSchema(IReadOnlyList<CommandSchema> schemas)
public void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input)
{
// Get command types marked as default
var defaultSchemas = schemas.Where(t => t.IsDefault).ToArray();
// If there's only one type - return
if (defaultSchemas.Length == 1)
return defaultSchemas.Single();
// If there are multiple - throw
if (defaultSchemas.Length > 1)
{
throw new CommandResolveException(
"Can't resolve default command because there is more than one command marked as default. " +
$"Make sure you apply {nameof(CommandAttribute)} only to one command.");
}
// If there aren't any - throw
throw new CommandResolveException(
"Can't resolve default command because there are no commands marked as default. " +
$"Apply {nameof(CommandAttribute)} to the default command.");
}
private CommandSchema GetSchemaByName(IReadOnlyList<CommandSchema> schemas, string name)
{
// Get command types with given name
var matchingSchemas =
schemas.Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray();
// If there's only one type - return
if (matchingSchemas.Length == 1)
return matchingSchemas.Single();
// If there are multiple - throw
if (matchingSchemas.Length > 1)
{
throw new CommandResolveException(
$"Can't resolve command because there is more than one command named [{name}]. " +
"Make sure all command names are unique and keep in mind that comparison is case-insensitive.");
}
// If there aren't any - throw
throw new CommandResolveException(
$"Can't resolve command because none of the commands is named [{name}]. " +
$"Apply {nameof(CommandAttribute)} to give command a name.");
}
// TODO: refactor
public ICommand InitializeCommand(CommandInput input)
{
var schemas = _commandSchemaResolver.ResolveAllSchemas();
// Get command type
var schema = !input.CommandName.IsNullOrWhiteSpace()
? GetSchemaByName(schemas, input.CommandName)
: GetDefaultSchema(schemas);
// Activate command
var command = (ICommand) _typeActivator.Activate(schema.Type);
command.Context = new CommandContext(schemas, schema);
// Set command options
var isGroupNameDetected = false;
var groupName = default(string);
var properties = new HashSet<CommandOptionSchema>();
foreach (var option in input.Options)
{
var optionInfo = schema.Options.FirstOrDefault(p =>
string.Equals(p.Name, option.Name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.ShortName?.AsString(), option.Name, StringComparison.OrdinalIgnoreCase));
var optionSchema = schema.Options.FindByAliasOrNull(option.Alias);
if (optionInfo == null)
if (optionSchema == null)
continue;
if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase))
if (isGroupNameDetected && !string.Equals(groupName, optionSchema.GroupName, StringComparison.OrdinalIgnoreCase))
continue;
if (!isGroupNameDetected)
{
groupName = optionInfo.GroupName;
groupName = optionSchema.GroupName;
isGroupNameDetected = true;
}
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType);
optionInfo.Property.SetValue(command, convertedValue);
var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionSchema.Property.PropertyType);
optionSchema.Property.SetValue(command, convertedValue);
properties.Add(optionInfo);
properties.Add(optionSchema);
}
var unsetRequiredOptions = schema.Options
@@ -128,10 +56,8 @@ namespace CliFx.Services
.ToArray();
if (unsetRequiredOptions.Any())
throw new CommandResolveException(
throw new MissingCommandOptionException(
$"Can't resolve command because one or more required properties were not set: {unsetRequiredOptions.Select(p => p.Name).JoinToString(", ")}");
return command;
}
}
}

View File

@@ -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}].");
}
}

View File

@@ -1,74 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CliFx.Attributes;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
public class CommandSchemaResolver : ICommandSchemaResolver
{
private readonly IReadOnlyList<Type> _sourceTypes;
public CommandSchemaResolver(IReadOnlyList<Type> sourceTypes)
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
{
_sourceTypes = sourceTypes;
var attribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
if (attribute == null)
return null;
return new CommandOptionSchema(optionProperty,
attribute.Name,
attribute.ShortName,
attribute.GroupName,
attribute.IsRequired, attribute.Description);
}
public CommandSchemaResolver(IReadOnlyList<Assembly> sourceAssemblies)
: this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray())
// TODO: validate stuff like duplicate names, multiple default commands, etc
public CommandSchema GetCommandSchema(Type commandType)
{
}
if (!commandType.Implements(typeof(ICommand)))
throw new ArgumentException($"Command type must implement {nameof(ICommand)}.", nameof(commandType));
public CommandSchemaResolver()
: this(new[] {Assembly.GetEntryAssembly()})
{
}
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
private IEnumerable<Type> GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand)));
var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
{
var result = new List<CommandOptionSchema>();
foreach (var optionProperty in commandType.GetProperties())
{
var optionAttribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
if (optionAttribute == null)
continue;
result.Add(new CommandOptionSchema(optionProperty,
optionAttribute.Name,
optionAttribute.ShortName,
optionAttribute.IsRequired,
optionAttribute.GroupName,
optionAttribute.Description));
}
return result;
}
public IReadOnlyList<CommandSchema> ResolveAllSchemas()
{
var result = new List<CommandSchema>();
foreach (var commandType in GetCommandTypes())
{
var commandAttribute = commandType.GetCustomAttribute<CommandAttribute>();
if (commandAttribute == null)
continue;
result.Add(new CommandSchema(commandType,
commandAttribute.Name,
commandAttribute.IsDefault,
commandAttribute.Description,
GetCommandOptionSchemas(commandType)));
}
return result;
return new CommandSchema(commandType,
attribute?.Name,
attribute?.Description,
options);
}
}
}

View File

@@ -1,11 +1,42 @@
using CliFx.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CliFx.Models;
namespace CliFx.Services
{
public static class Extensions
{
public static void Write(this IConsoleWriter consoleWriter, string text) => consoleWriter.Write(new TextSpan(text));
public static IReadOnlyList<CommandSchema> GetCommandSchemas(this ICommandSchemaResolver commandSchemaResolver,
IEnumerable<Type> commandTypes) => commandTypes.Select(commandSchemaResolver.GetCommandSchema).ToArray();
public static void WriteLine(this IConsoleWriter consoleWriter, string text) => consoleWriter.WriteLine(new TextSpan(text));
public static void Write(this IConsoleWriter consoleWriter, string text) =>
consoleWriter.Write(new TextSpan(text));
public static void Write(this IConsoleWriter consoleWriter, IFormattable formattable) =>
consoleWriter.Write(formattable.ToString(null, CultureInfo.InvariantCulture));
public static void Write(this IConsoleWriter consoleWriter, object obj)
{
if (obj is IFormattable formattable)
consoleWriter.Write(formattable);
else
consoleWriter.Write(obj.ToString());
}
public static void WriteLine(this IConsoleWriter consoleWriter, string text) =>
consoleWriter.WriteLine(new TextSpan(text));
public static void WriteLine(this IConsoleWriter consoleWriter, IFormattable formattable) =>
consoleWriter.WriteLine(formattable.ToString(null, CultureInfo.InvariantCulture));
public static void WriteLine(this IConsoleWriter consoleWriter, object obj)
{
if (obj is IFormattable formattable)
consoleWriter.WriteLine(formattable);
else
consoleWriter.WriteLine(obj.ToString());
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using CliFx.Models;
namespace CliFx.Services
{
public interface ICommandFactory
{
ICommand CreateCommand(CommandSchema schema);
}
}

View 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);
}
}

View File

@@ -4,6 +4,6 @@ namespace CliFx.Services
{
public interface ICommandInitializer
{
ICommand InitializeCommand(CommandInput input);
void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input);
}
}

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System;
using CliFx.Models;
namespace CliFx.Services
{
public interface ICommandSchemaResolver
{
IReadOnlyList<CommandSchema> ResolveAllSchemas();
CommandSchema GetCommandSchema(Type commandType);
}
}

View File

@@ -1,9 +0,0 @@
using CliFx.Models;
namespace CliFx.Services
{
public interface IHelpTextBuilder
{
string Build(CommandContext context);
}
}

View File

@@ -1,9 +0,0 @@
using System;
namespace CliFx.Services
{
public interface ITypeActivator
{
object Activate(Type type);
}
}

View File

@@ -1,9 +0,0 @@
using System;
namespace CliFx.Services
{
public class TypeActivator : ITypeActivator
{
public object Activate(Type type) => Activator.CreateInstance(type);
}
}

View File

@@ -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