This commit is contained in:
Alexey Golub
2020-01-27 21:10:14 +02:00
committed by GitHub
parent 63441688fe
commit 3883c831e9
122 changed files with 3472 additions and 4180 deletions

View File

@@ -1,35 +1,42 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using CliFx.Benchmarks.Commands; using CliFx.Benchmarks.Commands;
using CommandLine;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks
{ {
[SimpleJob] [SimpleJob]
[RankColumn] [RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Benchmark public class Benchmark
{ {
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"}; private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[Benchmark(Description = "CliFx", Baseline = true)] [Benchmark(Description = "CliFx", Baseline = true)]
public async ValueTask<int> ExecuteWithCliFx() => await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); public async ValueTask<int> ExecuteWithCliFx() =>
await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
[Benchmark(Description = "System.CommandLine")] [Benchmark(Description = "System.CommandLine")]
public async ValueTask<int> ExecuteWithSystemCommandLine() => await new SystemCommandLineCommand().ExecuteAsync(Arguments); public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments); public int ExecuteWithMcMaster() =>
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
[Benchmark(Description = "CommandLineParser")] [Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser() public void ExecuteWithCommandLineParser() =>
{ new CommandLine.Parser()
var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand)); .ParseArguments(Arguments, typeof(CommandLineParserCommand))
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute()); .WithParsed<CommandLineParserCommand>(c => c.Execute());
}
[Benchmark(Description = "PowerArgs")] [Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments); public void ExecuteWithPowerArgs() =>
PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
[Benchmark(Description = "Clipr")] [Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => clipr.CliParser.Parse<CliprCommand>(Arguments).Execute(); public void ExecuteWithClipr() =>
clipr.CliParser.Parse<CliprCommand>(Arguments).Execute();
} }
} }

View File

@@ -9,8 +9,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" /> <PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="clipr" Version="1.6.1" /> <PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.4.4" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.5.0" />
<PackageReference Include="PowerArgs" Version="3.6.0" /> <PackageReference Include="PowerArgs" Version="3.6.0" />
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" /> <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Benchmarks.Commands namespace CliFx.Benchmarks.Commands
{ {

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup> </ItemGroup>

View File

@@ -5,7 +5,6 @@ using CliFx.Demo.Internal;
using CliFx.Demo.Models; using CliFx.Demo.Models;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -14,17 +13,17 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandArgument(0, Name = "title", IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")] [CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
public string Author { get; set; } public string Author { get; set; } = "";
[CommandOption("published", 'p', Description = "Book publish date.")] [CommandOption("published", 'p', Description = "Book publish date.")]
public DateTimeOffset Published { get; set; } public DateTimeOffset Published { get; set; } = CreateRandomDate();
[CommandOption("isbn", 'n', Description = "Book ISBN.")] [CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn? Isbn { get; set; } public Isbn Isbn { get; set; } = CreateRandomIsbn();
public BookAddCommand(LibraryService libraryService) public BookAddCommand(LibraryService libraryService)
{ {
@@ -33,12 +32,6 @@ namespace CliFx.Demo.Commands
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set
if (Published == default)
Published = CreateRandomDate();
if (Isbn == default)
Isbn = CreateRandomIsbn();
if (_libraryService.GetBook(Title) != null) if (_libraryService.GetBook(Title) != null)
throw new CommandException("Book already exists.", 1); throw new CommandException("Book already exists.", 1);
@@ -65,7 +58,7 @@ namespace CliFx.Demo.Commands
Random.Next(1, 59), Random.Next(1, 59),
TimeSpan.Zero); TimeSpan.Zero);
public static Isbn CreateRandomIsbn() => new Isbn( private static Isbn CreateRandomIsbn() => new Isbn(
Random.Next(0, 999), Random.Next(0, 999),
Random.Next(0, 99), Random.Next(0, 99),
Random.Next(0, 99999), Random.Next(0, 99999),

View File

@@ -3,7 +3,6 @@ using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -12,8 +11,8 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
public BookCommand(LibraryService libraryService) public BookCommand(LibraryService libraryService)
{ {

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -11,8 +10,8 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
public BookRemoveCommand(LibraryService libraryService) public BookRemoveCommand(LibraryService libraryService)
{ {

View File

@@ -1,6 +1,5 @@
using System; using System;
using CliFx.Demo.Models; using CliFx.Demo.Models;
using CliFx.Services;
namespace CliFx.Demo.Internal namespace CliFx.Demo.Internal
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Globalization;
namespace CliFx.Demo.Models namespace CliFx.Demo.Models
{ {
@@ -24,21 +23,23 @@ namespace CliFx.Demo.Models
CheckDigit = checkDigit; CheckDigit = checkDigit;
} }
public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}"; public override string ToString() =>
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
} }
public partial class Isbn public partial class Isbn
{ {
public static Isbn Parse(string value) public static Isbn Parse(string value, IFormatProvider formatProvider)
{ {
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries); var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
return new Isbn( return new Isbn(
int.Parse(components[0], CultureInfo.InvariantCulture), int.Parse(components[0], formatProvider),
int.Parse(components[1], CultureInfo.InvariantCulture), int.Parse(components[1], formatProvider),
int.Parse(components[2], CultureInfo.InvariantCulture), int.Parse(components[2], formatProvider),
int.Parse(components[3], CultureInfo.InvariantCulture), int.Parse(components[3], formatProvider),
int.Parse(components[4], CultureInfo.InvariantCulture)); int.Parse(components[4], formatProvider)
);
} }
} }
} }

View File

@@ -8,7 +8,7 @@ namespace CliFx.Demo
{ {
public static class Program public static class Program
{ {
private static IServiceProvider ConfigureServices() private static IServiceProvider GetServiceProvider()
{ {
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -25,15 +25,11 @@ namespace CliFx.Demo
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
public static async Task<int> Main(string[] args) public static async Task<int> Main() =>
{ await new CliApplicationBuilder()
var serviceProvider = ConfigureServices();
return await new CliApplicationBuilder()
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type)) .UseTypeActivator(GetServiceProvider().GetService)
.Build() .Build()
.RunAsync(args); .RunAsync();
}
} }
} }

View File

@@ -2,6 +2,6 @@
Sample command line interface for managing a library of books. Sample command line interface for managing a library of books.
This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things. This demo project shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things.
You can get a list of available commands by running `CliFx.Demo --help`. You can get a list of available commands by running `CliFx.Demo --help`.

View File

@@ -25,7 +25,7 @@ namespace CliFx.Demo.Services
return JsonConvert.DeserializeObject<Library>(data); return JsonConvert.DeserializeObject<Library>(data);
} }
public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title); public Book? GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
public void AddBook(Book book) public void AddBook(Book book)
{ {

View File

@@ -1,8 +1,6 @@
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.IO; using System.IO;
using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
namespace CliFx.Tests namespace CliFx.Tests
@@ -10,9 +8,8 @@ namespace CliFx.Tests
[TestFixture] [TestFixture]
public class CliApplicationBuilderTests public class CliApplicationBuilderTests
{ {
// Make sure all builder methods work [Test(Description = "All builder methods must return without exceptions")]
[Test] public void Smoke_Test()
public void All_Smoke_Test()
{ {
// Arrange // Arrange
var builder = new CliApplicationBuilder(); var builder = new CliApplicationBuilder();
@@ -31,14 +28,11 @@ namespace CliFx.Tests
.UseVersionText("test") .UseVersionText("test")
.UseDescription("test") .UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null)) .UseConsole(new VirtualConsole(TextWriter.Null))
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!) .UseTypeActivator(Activator.CreateInstance)
.UseCommandOptionInputConverter(new CommandInputConverter())
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
.Build(); .Build();
} }
// Make sure builder can produce an application with no parameters specified [Test(Description = "Builder must be able to produce an application when no parameters are specified")]
[Test]
public void Build_Test() public void Build_Test()
{ {
// Arrange // Arrange

View File

@@ -5,8 +5,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
namespace CliFx.Tests namespace CliFx.Tests
@@ -14,6 +12,7 @@ namespace CliFx.Tests
[TestFixture] [TestFixture]
public class CliApplicationTests public class CliApplicationTests
{ {
private const string TestAppName = "TestApp";
private const string TestVersionText = "v1.0"; private const string TestVersionText = "v1.0";
private static IEnumerable<TestCaseData> GetTestCases_RunAsync() private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
@@ -21,102 +20,105 @@ namespace CliFx.Tests
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new string[0], new string[0],
new Dictionary<string, string>(),
"Hello world." "Hello world."
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "}, new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
new Dictionary<string, string>(),
"foo bar" "foo bar"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "one", "two", "three", "-s", ", "}, new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
new Dictionary<string, string>(),
"one, two, three" "one, two, three"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DivideCommand)}, new[] {typeof(DivideCommand)},
new[] {"div", "-D", "24", "-d", "8"}, new[] {"div", "-D", "24", "-d", "8"},
new Dictionary<string, string>(),
"3" "3"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--version"}, new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"--version"}, new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText TestVersionText
); );
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"-h"},
null
);
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--help"},
null
);
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new string[0], new string[0],
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"-h"}, new[] {"-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"--help"}, new[] {"--help"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-h"}, new[] {"concat", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] {typeof(ExceptionCommand)},
new[] {"exc", "-h"}, new[] {"exc", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-h"}, new[] {"exc", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"[preview]"}, new[] {"[preview]"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] {typeof(ExceptionCommand)},
new[] {"exc", "[preview]"}, new[] {"[preview]", "exc"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "[preview]", "-o", "value"}, new[] {"[preview]", "concat", "-o", "value"},
new Dictionary<string, string>(),
null null
); );
} }
@@ -126,109 +128,273 @@ namespace CliFx.Tests
yield return new TestCaseData( yield return new TestCaseData(
new Type[0], new Type[0],
new string[0], new string[0],
new Dictionary<string, string>(),
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"non-existing"}, new[] {"non-existing"},
new Dictionary<string, string>(),
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] {typeof(ExceptionCommand)},
new[] {"exc"}, new[] {"exc"},
new Dictionary<string, string>(),
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc"}, new[] {"exc"},
new Dictionary<string, string>(),
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc"}, new[] {"exc"},
new Dictionary<string, string>(),
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar"}, new[] {"exc", "-m", "foo bar"},
new Dictionary<string, string>(),
"foo bar", null "foo bar", null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar", "-c", "666"}, new[] {"exc", "-m", "foo bar", "-c", "666"},
new Dictionary<string, string>(),
"foo bar", 666 "foo bar", 666
); );
} }
[Test] private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Help()
{
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Description",
"HelpDefaultCommand description.",
"Usage",
TestAppName, "[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "HelpNamedCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Usage",
TestAppName, "[command]",
"Options",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "--help"},
new[]
{
"Description",
"HelpNamedCommand description.",
"Usage",
TestAppName, "cmd", "[command]", "[options]",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "sub", "--help"},
new[]
{
"Description",
"HelpSubCommand description.",
"Usage",
TestAppName, "cmd sub", "[options]",
"Options",
"-e|--option-e", "OptionE description.",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new[] {"param", "cmd", "--help"},
new[]
{
"Description",
"Command using positional parameters",
"Usage",
TestAppName, "param cmd", "<first>", "<PARAMETERB>", "<third list>", "[options]",
"Parameters",
"* first",
"* PARAMETERB",
"* third list", "A list of numbers",
"Options",
"-o|--option",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(AllRequiredOptionsCommand)},
new[] {"allrequired", "--help"},
new[]
{
"Description",
"AllRequiredOptionsCommand description.",
"Usage",
TestAppName, "allrequired --option-f <value> --option-g <value>"
}
);
yield return new TestCaseData(
new[] {typeof(SomeRequiredOptionsCommand)},
new[] {"somerequired", "--help"},
new[]
{
"Description",
"SomeRequiredOptionsCommand description.",
"Usage",
TestAppName, "somerequired --option-f <value> [options]"
}
);
}
[TestCaseSource(nameof(GetTestCases_RunAsync))] [TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, public async Task RunAsync_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdOut = null) string? expectedStdOut = null)
{ {
// Arrange // Arrange
await using var stdoutStream = new StringWriter(); await using var stdOutStream = new StringWriter();
var console = new VirtualConsole(stdOutStream);
var console = new VirtualConsole(stdoutStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText) .UseVersionText(TestVersionText)
.UseConsole(console) .UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdoutStream.ToString().Trim(); var stdOut = stdOutStream.ToString().Trim();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdOut.Should().NotBeNullOrWhiteSpace();
if (expectedStdOut != null) if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut); stdOut.Should().Be(expectedStdOut);
else
stdOut.Should().NotBeNullOrWhiteSpace(); Console.WriteLine(stdOut);
} }
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, public async Task RunAsync_Negative_Test(
string? expectedStdErr = null, int? expectedExitCode = null) IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdErr = null,
int? expectedExitCode = null)
{ {
// Arrange // Arrange
await using var stderrStream = new StringWriter(); await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(TextWriter.Null, stdErrStream);
var console = new VirtualConsole(TextWriter.Null, stderrStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText) .UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console) .UseConsole(console)
.Build(); .Build();
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stderr = stderrStream.ToString().Trim(); var stderr = stdErrStream.ToString().Trim();
// Assert // Assert
exitCode.Should().NotBe(0);
stderr.Should().NotBeNullOrWhiteSpace();
if (expectedExitCode != null) if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode); exitCode.Should().Be(expectedExitCode);
else
exitCode.Should().NotBe(0);
if (expectedStdErr != null) if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr); stderr.Should().Be(expectedStdErr);
else
stderr.Should().NotBeNullOrWhiteSpace(); Console.WriteLine(stderr);
}
[TestCaseSource(nameof(GetTestCases_RunAsync_Help))]
public async Task RunAsync_Help_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyList<string>? expectedSubstrings = null)
{
// Arrange
await using var stdOutStream = new StringWriter();
var console = new VirtualConsole(stdOutStream);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
var environmentVariables = new Dictionary<string, string>();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
// Assert
exitCode.Should().Be(0);
stdOut.Should().NotBeNullOrWhiteSpace();
if (expectedSubstrings != null)
stdOut.Should().ContainAll(expectedSubstrings);
Console.WriteLine(stdOut);
} }
[Test] [Test]
@@ -236,25 +402,31 @@ namespace CliFx.Tests
{ {
// Arrange // Arrange
using var cancellationTokenSource = new CancellationTokenSource(); using var cancellationTokenSource = new CancellationTokenSource();
await using var stdoutStream = new StringWriter();
var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token); await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand)) .AddCommand(typeof(CancellableCommand))
.UseConsole(console) .UseConsole(console)
.Build(); .Build();
var args = new[] {"cancel"};
var commandLineArguments = new[] {"cancel"};
var environmentVariables = new Dictionary<string, string>();
// Act // Act
var runTask = application.RunAsync(args); cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2));
cancellationTokenSource.Cancel(); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var exitCode = await runTask.ConfigureAwait(false); var stdOut = stdOutStream.ToString().Trim();
var stdOut = stdoutStream.ToString().Trim(); var stdErr = stdErrStream.ToString().Trim();
// Assert // Assert
exitCode.Should().Be(-2146233029); exitCode.Should().NotBe(0);
stdOut.Should().Be("Printed"); stdOut.Should().BeNullOrWhiteSpace();
stdErr.Should().NotBeNullOrWhiteSpace();
Console.WriteLine(stdErr);
} }
} }
} }

View File

@@ -11,11 +11,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.9.0" /> <PackageReference Include="FluentAssertions" Version="5.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="coverlet.msbuild" Version="2.7.0" PrivateAssets="all" /> <PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Tests.TestCommands;
using CliFx.Tests.TestCustomTypes;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DefaultCommandFactoryTests
{
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
{
yield return new TestCaseData(typeof(HelloWorldDefaultCommand));
}
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
{
yield return new TestCaseData(typeof(TestNonStringParseable));
}
[TestCaseSource(nameof(GetTestCases_CreateInstance))]
public void CreateInstance_Test(Type type)
{
// Arrange
var activator = new DefaultTypeActivator();
// Act
var obj = activator.CreateInstance(type);
// Assert
obj.Should().BeOfType(type);
}
[TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))]
public void CreateInstance_Negative_Test(Type type)
{
// Arrange
var activator = new DefaultTypeActivator();
// Act & Assert
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(type));
Console.WriteLine(ex.Message);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DelegateCommandFactoryTests
{
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{
yield return new TestCaseData(
new Func<Type, object>(Activator.CreateInstance),
typeof(HelloWorldDefaultCommand)
);
}
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
public void CreateCommand_Test(Func<Type, object> activatorFunc, Type type)
{
// Arrange
var activator = new DelegateTypeActivator(activatorFunc);
// Act
var obj = activator.CreateInstance(type);
// Assert
obj.Should().BeOfType(type);
}
}
}

View File

@@ -0,0 +1,888 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using CliFx.Domain;
using CliFx.Exceptions;
using CliFx.Tests.TestCommands;
using CliFx.Tests.TestCustomTypes;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Domain
{
[TestFixture]
internal partial class ApplicationSchemaTests
{
private static IEnumerable<TestCaseData> GetTestCases_Resolve()
{
yield return new TestCaseData(
new[]
{
typeof(DivideCommand),
typeof(ConcatCommand),
typeof(EnvironmentVariableCommand)
},
new[]
{
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new CommandParameterSchema[0], new[]
{
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
"dividend", 'D', null, true, "The number to divide."),
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
"divisor", 'd', null, true, "The number to divide by.")
}),
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
new CommandParameterSchema[0],
new[]
{
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
null, 'i', null, true, "Input strings."),
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
null, 's', null, false, "String separator.")
}),
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
new CommandParameterSchema[0],
new[]
{
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
"opt", null, "ENV_SINGLE_VALUE", false, null)
}
)
}
);
yield return new TestCaseData(
new[] {typeof(SimpleParameterCommand)},
new[]
{
new CommandSchema(typeof(SimpleParameterCommand), "param cmd2", "Command using positional parameters",
new[]
{
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterA)),
0, "first", null),
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterB)),
10, null, null)
},
new[]
{
new CommandOptionSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.OptionA)),
"option", 'o', null, false, null)
})
}
);
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[]
{
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null,
new CommandParameterSchema[0],
new CommandOptionSchema[0])
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_Resolve_Negative()
{
yield return new TestCaseData(new object[]
{
new Type[0]
});
// Command validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(NonImplementedCommand)}
});
yield return new TestCaseData(new object[]
{
// Same name
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(NonAnnotatedCommand)}
});
// Parameter validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateParameterOrderCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateParameterNameCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(MultipleNonScalarParametersCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(NonLastNonScalarParameterCommand)}
});
// Option validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionNamesCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionShortNamesCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}
});
}
[Test]
[TestCaseSource(nameof(GetTestCases_Resolve))]
public void Resolve_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<CommandSchema> expectedCommandSchemas)
{
// Act
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
// Assert
applicationSchema.Commands.Should().BeEquivalentTo(expectedCommandSchemas);
}
[Test]
[TestCaseSource(nameof(GetTestCases_Resolve_Negative))]
public void Resolve_Negative_Test(IReadOnlyList<Type> commandTypes)
{
// Act & Assert
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
Console.WriteLine(ex.Message);
}
}
internal partial class ApplicationSchemaTests
{
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint()
{
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Object), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Object = "value"}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.String), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {String = "value"}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "true")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = true}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "false")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = false}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = true}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Char), "a")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Char = 'a'}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Sbyte), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Sbyte = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Byte), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Byte = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Short), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Short = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ushort), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Ushort = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Int = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Uint), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Uint = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Long), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Long = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ulong), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Ulong = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Float), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Float = 123.45f}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Double), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Double = 123.45}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Decimal), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Decimal = 123.45m}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {DateTime = new DateTime(1995, 04, 28)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {DateTimeOffset = new DateTime(1995, 04, 28)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpan = new TimeSpan(00, 14, 59)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnum), "value2")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnum = TestEnum.Value2}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable), "666")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullable = 666}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable), "value3")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumNullable = TestEnum.Value3}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable), "01:00:00")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpanNullable = new TimeSpan(01, 00, 00)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpanNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructable), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestStringConstructable = new TestStringConstructable("value")}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseable), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestStringParseable = TestStringParseable.Parse("value")}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand
{
TestStringParseableWithFormatProvider =
TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture)
}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.ObjectArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {ObjectArray = new object[] {"value1", "value2"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringArray = new[] {"value1", "value2"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringArray = new string[0]}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntArray), new[] {"47", "69"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntArray = new[] {47, 69}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumArray), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumArray = new[] {TestEnum.Value1, TestEnum.Value3}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullableArray), new[] {"1337", "2441"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullableArray = new int?[] {1337, 2441}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructableArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand
{
TestStringConstructableArray = new[]
{
new TestStringConstructable("value1"),
new TestStringConstructable("value2")
}
}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Enumerable), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Enumerable = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringEnumerable), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringEnumerable = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringReadOnlyList), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringReadOnlyList = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringList), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringList = new List<string> {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringHashSet), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringHashSet = new HashSet<string> {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[] {new CommandOptionInput("i", new[] {"foo", " ", "bar"}),}),
new Dictionary<string, string>(),
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[]
{
new CommandOptionInput("i", new[] {"foo", "bar"}),
new CommandOptionInput("s", " "),
}),
new Dictionary<string, string>(),
new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_SINGLE_VALUE"] = "A"
},
new EnvironmentVariableCommand {Option = "A"}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableWithMultipleValuesCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
},
new EnvironmentVariableWithMultipleValuesCommand {Option = new[] {"A", "B", "C"}}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
new CommandLineInput(new[] {new CommandOptionInput("opt", "X")}),
new Dictionary<string, string>
{
["ENV_SINGLE_VALUE"] = "A"
},
new EnvironmentVariableCommand {Option = "X"}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableWithoutCollectionPropertyCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
},
new EnvironmentVariableWithoutCollectionPropertyCommand {Option = string.Join(Path.PathSeparator, "A", "B", "C")}
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd", "abc", "123", "1", "2"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>(),
new ParameterCommand
{
ParameterA = "abc",
ParameterB = 123,
ParameterC = new[] {1, 2},
OptionA = "option value"
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint_Negative()
{
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "1234.5")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), new[] {"123", "456"})}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int))}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.NonConvertible), "123")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"div"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"div", "-D", "13"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(new[] {"concat"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[] {new CommandOptionInput("s", "_")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd", "abc", "123", "invalid"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"non-existing"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(BrokenEnumerableCommand)},
new CommandLineInput(new[] {"value1", "value2"}),
new Dictionary<string, string>()
);
}
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint))]
public void InitializeEntryPoint_Test(
IReadOnlyList<Type> commandTypes,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables,
ICommand expectedResult)
{
// Arrange
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
var typeActivator = new DefaultTypeActivator();
// Act
var command = applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator);
// Assert
command.Should().BeEquivalentTo(expectedResult, o => o.RespectingRuntimeTypes());
}
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint_Negative))]
public void InitializeEntryPoint_Negative_Test(
IReadOnlyList<Type> commandTypes,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables)
{
// Arrange
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
var typeActivator = new DefaultTypeActivator();
// Act & Assert
var ex = Assert.Throws<CliFxException>(() =>
applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator));
Console.WriteLine(ex.Message);
}
}
}

View File

@@ -0,0 +1,264 @@
using System.Collections.Generic;
using CliFx.Domain;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Domain
{
[TestFixture]
internal class CommandLineInputTests
{
private static IEnumerable<TestCaseData> GetTestCases_Parse()
{
yield return new TestCaseData(
new string[0],
CommandLineInput.Empty
);
yield return new TestCaseData(
new[] {"param"},
new CommandLineInput(
new[] {"param"})
);
yield return new TestCaseData(
new[] {"cmd", "param"},
new CommandLineInput(
new[] {"cmd", "param"})
);
yield return new TestCaseData(
new[] {"--option", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "--option2", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("option2", "value2")
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "--option", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", "value")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-b", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", "value1"),
new CommandOptionInput("b", "value2")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-a", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "-b", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("b", "value2")
})
);
yield return new TestCaseData(
new[] {"--switch"},
new CommandLineInput(
new[]
{
new CommandOptionInput("switch")
})
);
yield return new TestCaseData(
new[] {"--switch1", "--switch2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("switch1"),
new CommandOptionInput("switch2")
})
);
yield return new TestCaseData(
new[] {"-s"},
new CommandLineInput(
new[]
{
new CommandOptionInput("s")
})
);
yield return new TestCaseData(
new[] {"-a", "-b"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
);
yield return new TestCaseData(
new[] {"-ab"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
);
yield return new TestCaseData(
new[] {"-ab", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "--option", "value"},
new CommandLineInput(
new[] {"cmd"},
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"[debug]"},
new CommandLineInput(
new[] {"debug"},
new string[0],
new CommandOptionInput[0])
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]"},
new CommandLineInput(
new[] {"debug", "preview"},
new string[0],
new CommandOptionInput[0])
);
yield return new TestCaseData(
new[] {"cmd", "param1", "param2", "--option", "value"},
new CommandLineInput(
new[] {"cmd", "param1", "param2"},
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new string[0],
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd"},
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd"},
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "param", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd", "param"},
new[]
{
new CommandOptionInput("o", "value")
})
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_Parse))]
public void Parse_Test(IReadOnlyList<string> commandLineArguments, CommandLineInput expectedResult)
{
// Act
var result = CommandLineInput.Parse(commandLineArguments);
// Assert
result.Should().BeEquivalentTo(expectedResult);
}
}
}

View File

@@ -1,132 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandArgumentSchemasValidatorTests
{
private static CommandArgumentSchema GetValidArgumentSchema(string propertyName, string name, bool isRequired, int order, string? description = null)
{
return new CommandArgumentSchema(typeof(TestCommand).GetProperty(propertyName)!, name, isRequired, description, order);
}
private static IEnumerable<TestCaseData> GetTestCases_ValidatorTest()
{
// Validation should succeed when no arguments are supplied
yield return new TestCaseData(new ValidatorTest(new List<CommandArgumentSchema>(), true));
// Multiple sequence arguments
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "B", false, 1)
}, false));
// Argument after sequence
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1)
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
}, true));
// Required arguments must appear before optional arguments
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1)
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", true, 2),
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
}, true));
// Argument order must be unique
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2)
}, true));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 1)
}, false));
// No arguments with the same name
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
}, false));
}
private class TestCommand
{
public IEnumerable<int> EnumerableProperty { get; set; }
public string StringProperty { get; set; }
}
public class ValidatorTest
{
public ValidatorTest(IReadOnlyCollection<CommandArgumentSchema> schemas, bool succeedsValidation)
{
Schemas = schemas;
SucceedsValidation = succeedsValidation;
}
public IReadOnlyCollection<CommandArgumentSchema> Schemas { get; }
public bool SucceedsValidation { get; }
}
[Test]
[TestCaseSource(nameof(GetTestCases_ValidatorTest))]
public void Validation_Test(ValidatorTest testCase)
{
// Arrange
var validator = new CommandArgumentSchemasValidator();
// Act
var result = validator.ValidateArgumentSchemas(testCase.Schemas);
// Assert
result.Any().Should().Be(!testCase.SucceedsValidation);
}
}
}

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandFactoryTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{
yield return new TestCaseData(GetCommandSchema(typeof(HelloWorldDefaultCommand)));
}
[Test]
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
public void CreateCommand_Test(CommandSchema commandSchema)
{
// Arrange
var factory = new CommandFactory();
// Act
var command = factory.CreateCommand(commandSchema);
// Assert
command.Should().BeOfType(commandSchema.Type);
}
}
}

View File

@@ -1,245 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using CliFx.Tests.Stubs;
using System.IO;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandInitializerTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] { commandType }).Single();
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
{
yield return new TestCaseData(
new DivideCommand(),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new ConcatCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("i", new[] { "foo", " ", "bar" })
})),
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
);
yield return new TestCaseData(
new ConcatCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("i", new[] { "foo", "bar" }),
new CommandOptionInput("s", " ")
})),
new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " }
);
//Will read a value from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableCommand { Option = "A" }
);
//Will read multiple values from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableWithMultipleValuesCommand(),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } }
);
//Will not read a value from environment variables because one is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new string[0],
new CommandInput(new string[0], new[]
{
new CommandOptionInput("opt", new[] { "X" })
},
EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableCommand { Option = "X" }
);
//Will not split environment variable values because underlying property is not a collection
yield return new TestCaseData(
new EnvironmentVariableWithoutCollectionPropertyCommand(),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
);
// Positional arguments
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new [] { "abc", "123", "1", "2" },
new CommandInput(new [] { "arg", "cmd", "abc", "123", "1", "2" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
new ArgumentCommand { FirstArgument = "abc", SecondArgument = 123, ThirdArguments = new List<int>{1, 2}, Option = "option value" }
);
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new [] { "abc" },
new CommandInput(new [] { "arg", "cmd", "abc" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
new ArgumentCommand { FirstArgument = "abc", Option = "option value" }
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
{
yield return new TestCaseData(
new DivideCommand(),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" })
));
yield return new TestCaseData(
new DivideCommand(),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("D", "13")
})
));
yield return new TestCaseData(
new ConcatCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" })
));
yield return new TestCaseData(
new ConcatCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("s", "_")
})
));
// Missing required positional argument
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new string[0],
new CommandInput(new string[0], new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
// Incorrect data type in list
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new []{ "abc", "123", "invalid" },
new CommandInput(new [] { "arg", "cmd", "abc", "123", "invalid" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
// Extraneous unused arguments
yield return new TestCaseData(
new SimpleArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(SimpleArgumentCommand)),
new []{ "abc", "123", "unused" },
new CommandInput(new [] { "arg", "cmd2", "abc", "123", "unused" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
public void InitializeCommand_Test(ICommand command, CommandCandidate commandCandidate,
ICommand expectedCommand)
{
// Arrange
var initializer = new CommandInitializer();
// Act
initializer.InitializeCommand(command, commandCandidate);
// Assert
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
public void InitializeCommand_Negative_Test(ICommand command, CommandCandidate commandCandidate)
{
// Arrange
var initializer = new CommandInitializer();
// Act & Assert
initializer.Invoking(i => i.InitializeCommand(command, commandCandidate))
.Should().ThrowExactly<CliFxException>();
}
}
}

View File

@@ -1,323 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCustomTypes;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandInputConverterTests
{
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
{
yield return new TestCaseData(
new CommandOptionInput("option", "value"),
typeof(string),
"value"
);
yield return new TestCaseData(
new CommandOptionInput("option", "value"),
typeof(object),
"value"
);
yield return new TestCaseData(
new CommandOptionInput("option", "true"),
typeof(bool),
true
);
yield return new TestCaseData(
new CommandOptionInput("option", "false"),
typeof(bool),
false
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(bool),
true
);
yield return new TestCaseData(
new CommandOptionInput("option", "a"),
typeof(char),
'a'
);
yield return new TestCaseData(
new CommandOptionInput("option", "15"),
typeof(sbyte),
(sbyte) 15
);
yield return new TestCaseData(
new CommandOptionInput("option", "15"),
typeof(byte),
(byte) 15
);
yield return new TestCaseData(
new CommandOptionInput("option", "15"),
typeof(short),
(short) 15
);
yield return new TestCaseData(
new CommandOptionInput("option", "15"),
typeof(ushort),
(ushort) 15
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(int),
123
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(uint),
123u
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(long),
123L
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(ulong),
123UL
);
yield return new TestCaseData(
new CommandOptionInput("option", "123.45"),
typeof(float),
123.45f
);
yield return new TestCaseData(
new CommandOptionInput("option", "123.45"),
typeof(double),
123.45
);
yield return new TestCaseData(
new CommandOptionInput("option", "123.45"),
typeof(decimal),
123.45m
);
yield return new TestCaseData(
new CommandOptionInput("option", "28 Apr 1995"),
typeof(DateTime),
new DateTime(1995, 04, 28)
);
yield return new TestCaseData(
new CommandOptionInput("option", "28 Apr 1995"),
typeof(DateTimeOffset),
new DateTimeOffset(new DateTime(1995, 04, 28))
);
yield return new TestCaseData(
new CommandOptionInput("option", "00:14:59"),
typeof(TimeSpan),
new TimeSpan(00, 14, 59)
);
yield return new TestCaseData(
new CommandOptionInput("option", "value2"),
typeof(TestEnum),
TestEnum.Value2
);
yield return new TestCaseData(
new CommandOptionInput("option", "666"),
typeof(int?),
666
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(int?),
null
);
yield return new TestCaseData(
new CommandOptionInput("option", "value3"),
typeof(TestEnum?),
TestEnum.Value3
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(TestEnum?),
null
);
yield return new TestCaseData(
new CommandOptionInput("option", "01:00:00"),
typeof(TimeSpan?),
new TimeSpan(01, 00, 00)
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(TimeSpan?),
null
);
yield return new TestCaseData(
new CommandOptionInput("option", "value"),
typeof(TestStringConstructable),
new TestStringConstructable("value")
);
yield return new TestCaseData(
new CommandOptionInput("option", "value"),
typeof(TestStringParseable),
TestStringParseable.Parse("value")
);
yield return new TestCaseData(
new CommandOptionInput("option", "value"),
typeof(TestStringParseableWithFormatProvider),
TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture)
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(string[]),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(object[]),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"47", "69"}),
typeof(int[]),
new[] {47, 69}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"47"}),
typeof(int[]),
new[] {47}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value3"}),
typeof(TestEnum[]),
new[] {TestEnum.Value1, TestEnum.Value3}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"1337", "2441"}),
typeof(int?[]),
new int?[] {1337, 2441}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(TestStringConstructable[]),
new[] {new TestStringConstructable("value1"), new TestStringConstructable("value2")}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(IEnumerable),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(IEnumerable<string>),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(IReadOnlyList<string>),
new[] {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(List<string>),
new List<string> {"value1", "value2"}
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value2"}),
typeof(HashSet<string>),
new HashSet<string> {"value1", "value2"}
);
}
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput_Negative()
{
yield return new TestCaseData(
new CommandOptionInput("option", "1234.5"),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"123", "456"}),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option", "123"),
typeof(TestNonStringParseable)
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_ConvertOptionInput))]
public void ConvertOptionInput_Test(CommandOptionInput optionInput, Type targetType,
object expectedConvertedValue)
{
// Arrange
var converter = new CommandInputConverter();
// Act
var convertedValue = converter.ConvertOptionInput(optionInput, targetType);
// Assert
convertedValue.Should().BeEquivalentTo(expectedConvertedValue);
convertedValue?.Should().BeAssignableTo(targetType);
}
[Test]
[TestCaseSource(nameof(GetTestCases_ConvertOptionInput_Negative))]
public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType)
{
// Arrange
var converter = new CommandInputConverter();
// Act & Assert
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
.Should().ThrowExactly<CliFxException>();
}
}
}

View File

@@ -1,255 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using System.Collections.Generic;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.Stubs;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandInputParserTests
{
private static IEnumerable<TestCaseData> GetTestCases_ParseCommandInput()
{
yield return new TestCaseData(new string[0], CommandInput.Empty, new EmptyEnvironmentVariablesProviderStub());
yield return new TestCaseData(
new[] { "--option", "value" },
new CommandInput(new[]
{
new CommandOptionInput("option", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--option1", "value1", "--option2", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("option2", "value2")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--option", "value1", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--option", "value1", "--option", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-a", "value" },
new CommandInput(new[]
{
new CommandOptionInput("a", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-a", "value1", "-b", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", "value1"),
new CommandOptionInput("b", "value2")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-a", "value1", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-a", "value1", "-a", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--option1", "value1", "-b", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("b", "value2")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--switch" },
new CommandInput(new[]
{
new CommandOptionInput("switch")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "--switch1", "--switch2" },
new CommandInput(new[]
{
new CommandOptionInput("switch1"),
new CommandOptionInput("switch2")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-s" },
new CommandInput(new[]
{
new CommandOptionInput("s")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-a", "-b" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-ab" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "-ab", "value" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "command" },
new CommandInput(new []{ "command" }),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "command", "--option", "value" },
new CommandInput(new []{ "command" }, new[]
{
new CommandOptionInput("option", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "long", "command", "name" },
new CommandInput(new []{ "long", "command", "name"}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "long", "command", "name", "--option", "value" },
new CommandInput(new []{ "long", "command", "name" }, new[]
{
new CommandOptionInput("option", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "[debug]" },
new CommandInput(new string[0],
new[] { "debug" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "[debug]", "[preview]" },
new CommandInput(new string[0],
new[] { "debug", "preview" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "[debug]", "[preview]", "-o", "value" },
new CommandInput(new string[0],
new[] { "debug", "preview" },
new[]
{
new CommandOptionInput("o", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "command", "[debug]", "[preview]", "-o", "value" },
new CommandInput(new []{"command"},
new[] { "debug", "preview" },
new[]
{
new CommandOptionInput("o", "value")
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "command", "[debug]", "[preview]", "-o", "value" },
new CommandInput(new []{ "command"},
new[] { "debug", "preview" },
new[]
{
new CommandOptionInput("o", "value")
},
EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariablesProviderStub()
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_ParseCommandInput))]
public void ParseCommandInput_Test(IReadOnlyList<string> commandLineArguments,
CommandInput expectedCommandInput, IEnvironmentVariablesProvider environmentVariablesProvider)
{
// Arrange
var parser = new CommandInputParser(environmentVariablesProvider);
// Act
var commandInput = parser.ParseCommandInput(commandLineArguments);
// Assert
commandInput.Should().BeEquivalentTo(expectedCommandInput);
}
}
}

View File

@@ -1,306 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandSchemaResolverTests
{
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
{
yield return new TestCaseData(
new[] { typeof(DivideCommand), typeof(ConcatCommand), typeof(EnvironmentVariableCommand) },
new[]
{
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new CommandArgumentSchema[0], new[]
{
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
"dividend", 'D', true, "The number to divide.", null),
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
"divisor", 'd', true, "The number to divide by.", null)
}),
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
new CommandArgumentSchema[0],
new[]
{
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
null, 'i', true, "Input strings.", null),
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
null, 's', false, "String separator.", null)
}),
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
new CommandArgumentSchema[0],
new[]
{
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
"opt", null, false, null, "ENV_SINGLE_VALUE")
}
)
}
);
yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) },
new[]
{
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0])
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
{
yield return new TestCaseData(new object[]
{
new Type[0]
});
yield return new TestCaseData(new object[]
{
new[] { typeof(NonImplementedCommand) }
});
yield return new TestCaseData(new object[]
{
new[] { typeof(NonAnnotatedCommand) }
});
yield return new TestCaseData(new object[]
{
new[] { typeof(DuplicateOptionNamesCommand) }
});
yield return new TestCaseData(new object[]
{
new[] { typeof(DuplicateOptionShortNamesCommand) }
});
yield return new TestCaseData(new object[]
{
new[] { typeof(ExceptionCommand), typeof(CommandExceptionCommand) }
});
}
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Positive()
{
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null)
},
new CommandInput(new[] { "command1", "argument1", "argument2" }),
new[] { "argument1", "argument2" },
"command1"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null)
},
new CommandInput(new[] { "argument1", "argument2" }),
new[] { "argument1", "argument2" },
""
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1 subcommand1", null, null, null),
},
new CommandInput(new[] { "command1", "subcommand1", "argument1" }),
new[] { "argument1" },
"command1 subcommand1"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b", "d" }),
new[] { "d" },
"a b"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b", "c", "d" }),
new[] { "d" },
"a b c"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "b", "c" }),
new string[0],
"b c"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "d", "a", "b"}),
new[] { "d", "a", "b" },
""
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b c", "d" }),
new[] { "b c", "d" },
"a"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a b", "c", "d" }),
new[] { "a b", "c", "d" },
""
);
}
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Negative()
{
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null),
},
new CommandInput(new[] { "command4", "argument1" })
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null),
},
new CommandInput(new[] { "argument1" })
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1 subcommand1", null, null, null),
},
new CommandInput(new[] { "command1", "argument1" })
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
IReadOnlyList<CommandSchema> expectedCommandSchemas)
{
// Arrange
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
// Assert
commandSchemas.Should().BeEquivalentTo(expectedCommandSchemas);
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas_Negative))]
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
{
// Arrange
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act & Assert
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
.Should().ThrowExactly<CliFxException>();
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Positive))]
public void GetTargetCommandSchema_Positive_Test(IReadOnlyList<CommandSchema> availableCommandSchemas,
CommandInput commandInput,
IReadOnlyList<string> expectedPositionalArguments,
string expectedCommandSchemaName)
{
// Arrange
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Assert
commandCandidate.Should().NotBeNull();
commandCandidate.PositionalArgumentsInput.Should().BeEquivalentTo(expectedPositionalArguments);
commandCandidate.Schema.Name.Should().Be(expectedCommandSchemaName);
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Negative))]
public void GetTargetCommandSchema_Negative_Test(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput)
{
// Arrange
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Assert
commandCandidate.Should().BeNull();
}
}
}

View File

@@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class DelegateCommandFactoryTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{
yield return new TestCaseData(
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type!)!),
GetCommandSchema(typeof(HelloWorldDefaultCommand))
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
public void CreateCommand_Test(Func<CommandSchema, ICommand> factoryMethod, CommandSchema commandSchema)
{
// Arrange
var factory = new DelegateCommandFactory(factoryMethod);
// Act
var command = factory.CreateCommand(commandSchema);
// Assert
command.Should().BeOfType(commandSchema.Type);
}
}
}

View File

@@ -1,177 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class HelpTextRendererTests
{
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
{
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null);
var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes);
var targetCommandSchema = availableCommandSchemas.Single(s => s.Type == targetCommandType);
return new HelpTextSource(applicationMetadata, availableCommandSchemas, targetCommandSchema);
}
private static IEnumerable<TestCaseData> GetTestCases_RenderHelpText()
{
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
typeof(HelpDefaultCommand)),
new[]
{
"Description",
"HelpDefaultCommand description.",
"Usage",
"[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "HelpNamedCommand description.",
"You can run", "to show help on a specific command."
},
new string[0]
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
typeof(HelpNamedCommand)),
new[]
{
"Description",
"HelpNamedCommand description.",
"Usage",
"cmd", "[command]", "[options]",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
},
new string[0]
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
typeof(HelpSubCommand)),
new[]
{
"Description",
"HelpSubCommand description.",
"Usage",
"cmd sub", "[options]",
"Options",
"-e|--option-e", "OptionE description.",
"-h|--help", "Shows help text."
},
new string[0]
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(ArgumentCommand)},
typeof(ArgumentCommand)),
new[]
{
"Description",
"Command using positional arguments",
"Usage",
"arg cmd", "<first>", "[<secondargument>]", "[<third list>]", "[options]",
"Arguments",
"* first",
"secondargument",
"third list", "A list of numbers",
"Options",
"-o|--option",
"-h|--help", "Shows help text."
},
new string[0]
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] { typeof(AllRequiredOptionsCommand) },
typeof(AllRequiredOptionsCommand)),
new[]
{
"Description",
"AllRequiredOptionsCommand description.",
"Usage",
"testapp allrequired --option-f <value> --option-g <value>"
},
new []
{
"[options]"
}
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] { typeof(SomeRequiredOptionsCommand) },
typeof(SomeRequiredOptionsCommand)),
new[]
{
"Description",
"SomeRequiredOptionsCommand description.",
"Usage",
"testapp somerequired --option-f <value> [options]"
},
new string[0]
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_RenderHelpText))]
public void RenderHelpText_Test(HelpTextSource source,
IReadOnlyList<string> expectedSubstrings,
IReadOnlyList<string> notExpectedSubstrings)
{
// Arrange
using var stdout = new StringWriter();
var console = new VirtualConsole(stdout);
var renderer = new HelpTextRenderer();
// Act
renderer.RenderHelpText(console, source);
// Assert
stdout.ToString().Should().ContainAll(expectedSubstrings);
if (notExpectedSubstrings != null && notExpectedSubstrings.Any())
{
stdout.ToString().Should().NotContainAll(notExpectedSubstrings);
}
}
}
}

View File

@@ -1,10 +0,0 @@
using System.Collections.Generic;
using CliFx.Services;
namespace CliFx.Tests.Stubs
{
public class EmptyEnvironmentVariablesProviderStub : IEnvironmentVariablesProvider
{
public IReadOnlyDictionary<string, string> GetEnvironmentVariables() => new Dictionary<string, string>();
}
}

View File

@@ -1,18 +0,0 @@
using System.Collections.Generic;
using System.IO;
using CliFx.Services;
namespace CliFx.Tests.Stubs
{
public class EnvironmentVariablesProviderStub : IEnvironmentVariablesProvider
{
public static readonly Dictionary<string, string> EnvironmentVariables = new Dictionary<string, string>
{
["ENV_SINGLE_VALUE"] = "A",
["ENV_MULTIPLE_VALUES"] = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}",
["ENV_ESCAPED_MULTIPLE_VALUES"] = $"\"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}\""
};
public IReadOnlyDictionary<string, string> GetEnvironmentVariables() => EnvironmentVariables;
}
}

View File

@@ -1,9 +1,8 @@
using System; using System;
using CliFx.Services;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
namespace CliFx.Tests.Services namespace CliFx.Tests
{ {
[TestFixture] [TestFixture]
public class SystemConsoleTests public class SystemConsoleTests
@@ -11,13 +10,12 @@ namespace CliFx.Tests.Services
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
// Reset console color so it doesn't carry on into next tests // Reset console color so it doesn't carry on into the next tests
Console.ResetColor(); Console.ResetColor();
} }
// Make sure console correctly wraps around System.Console [Test(Description = "Must be in sync with system console")]
[Test] public void Smoke_Test()
public void All_Smoke_Test()
{ {
// Arrange // Arrange
var console = new SystemConsole(); var console = new SystemConsole();

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Tests.TestCustomTypes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class AllSupportedTypesCommand : ICommand
{
[CommandOption(nameof(Object))]
public object? Object { get; set; } = 42;
[CommandOption(nameof(String))]
public string? String { get; set; } = "foo bar";
[CommandOption(nameof(Bool))]
public bool Bool { get; set; }
[CommandOption(nameof(Char))]
public char Char { get; set; }
[CommandOption(nameof(Sbyte))]
public sbyte Sbyte { get; set; }
[CommandOption(nameof(Byte))]
public byte Byte { get; set; }
[CommandOption(nameof(Short))]
public short Short { get; set; }
[CommandOption(nameof(Ushort))]
public ushort Ushort { get; set; }
[CommandOption(nameof(Int))]
public int Int { get; set; }
[CommandOption(nameof(Uint))]
public uint Uint { get; set; }
[CommandOption(nameof(Long))]
public long Long { get; set; }
[CommandOption(nameof(Ulong))]
public ulong Ulong { get; set; }
[CommandOption(nameof(Float))]
public float Float { get; set; }
[CommandOption(nameof(Double))]
public double Double { get; set; }
[CommandOption(nameof(Decimal))]
public decimal Decimal { get; set; }
[CommandOption(nameof(DateTime))]
public DateTime DateTime { get; set; }
[CommandOption(nameof(DateTimeOffset))]
public DateTimeOffset DateTimeOffset { get; set; }
[CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; }
[CommandOption(nameof(TestEnum))]
public TestEnum TestEnum { get; set; }
[CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; }
[CommandOption(nameof(TestEnumNullable))]
public TestEnum? TestEnumNullable { get; set; }
[CommandOption(nameof(TimeSpanNullable))]
public TimeSpan? TimeSpanNullable { get; set; }
[CommandOption(nameof(TestStringConstructable))]
public TestStringConstructable? TestStringConstructable { get; set; }
[CommandOption(nameof(TestStringParseable))]
public TestStringParseable? TestStringParseable { get; set; }
[CommandOption(nameof(TestStringParseableWithFormatProvider))]
public TestStringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; }
[CommandOption(nameof(ObjectArray))]
public object[]? ObjectArray { get; set; }
[CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; }
[CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; }
[CommandOption(nameof(TestEnumArray))]
public TestEnum[]? TestEnumArray { get; set; }
[CommandOption(nameof(IntNullableArray))]
public int?[]? IntNullableArray { get; set; }
[CommandOption(nameof(TestStringConstructableArray))]
public TestStringConstructable[]? TestStringConstructableArray { get; set; }
[CommandOption(nameof(Enumerable))]
public IEnumerable? Enumerable { get; set; }
[CommandOption(nameof(StringEnumerable))]
public IEnumerable<string>? StringEnumerable { get; set; }
[CommandOption(nameof(StringReadOnlyList))]
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
[CommandOption(nameof(StringList))]
public List<string>? StringList { get; set; }
[CommandOption(nameof(StringHashSet))]
public HashSet<string>? StringHashSet { get; set; }
[CommandOption(nameof(NonConvertible))]
public TestNonStringParseable? NonConvertible { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,25 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("arg cmd", Description = "Command using positional arguments")]
public class ArgumentCommand : ICommand
{
[CommandArgument(0, IsRequired = true, Name = "first")]
public string? FirstArgument { get; set; }
[CommandArgument(10)]
public int? SecondArgument { get; set; }
[CommandArgument(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<int> ThirdArguments { get; set; }
[CommandOption("option", 'o')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Tests.TestCustomTypes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class BrokenEnumerableCommand : ICommand
{
[CommandParameter(0)]
public TestCustomEnumerable<string>? Test { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -10,13 +9,8 @@ namespace CliFx.Tests.TestCommands
{ {
public async ValueTask ExecuteAsync(IConsole console) public async ValueTask ExecuteAsync(IConsole console)
{ {
await Task.Yield(); await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), console.GetCancellationToken()).ConfigureAwait(false);
console.Output.WriteLine("Never printed"); console.Output.WriteLine("Never printed");
} }
} }
} }

View File

@@ -1,7 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
{
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
public string? OptionA { get; set; }
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,18 +1,17 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
[Command] [Command]
public class DuplicateOptionShortNamesCommand : ICommand public class DuplicateOptionShortNamesCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('x')]
public string? Apples { get; set; } public string? OptionA { get; set; }
[CommandOption('f')] [CommandOption('x')]
public string? Oranges { get; set; } public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateParameterNameCommand : ICommand
{
[CommandParameter(0, Name = "param")]
public string? ParameterA { get; set; }
[CommandParameter(1, Name = "param")]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateParameterOrderCommand : ICommand
{
[CommandParameter(13)]
public string? ParameterA { get; set; }
[CommandParameter(13)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class MultipleNonScalarParametersCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string>? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,5 +1,4 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class NonLastNonScalarParameterCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("param cmd", Description = "Command using positional parameters")]
public class ParameterCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandParameter(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<int>? ParameterC { get; set; }
[CommandOption("option", 'o')]
public string? OptionA { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,21 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("arg cmd2", Description = "Command using positional arguments")]
public class SimpleArgumentCommand : ICommand
{
[CommandArgument(0, IsRequired = true, Name = "first")]
public string? FirstArgument { get; set; }
[CommandArgument(10)]
public int? SecondArgument { get; set; }
[CommandOption("option", 'o')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("param cmd2", Description = "Command using positional parameters")]
public class SimpleParameterCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandOption("option", 'o')]
public string? OptionA { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {

View File

@@ -0,0 +1,14 @@
using System.Collections;
using System.Collections.Generic;
namespace CliFx.Tests.TestCustomTypes
{
public class TestCustomEnumerable<T> : IEnumerable<T>
{
private readonly T[] _arr = new T[0];
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _arr).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -1,7 +1,6 @@
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using CliFx.Services;
using CliFx.Utilities; using CliFx.Utilities;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;

View File

@@ -1,17 +1,15 @@
using System; using System;
using System.IO; using System.IO;
using CliFx.Services;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
namespace CliFx.Tests.Services namespace CliFx.Tests
{ {
[TestFixture] [TestFixture]
public class VirtualConsoleTests public class VirtualConsoleTests
{ {
// Make sure console uses specified streams and doesn't leak to System.Console [Test(Description = "Must not leak to system console")]
[Test] public void Smoke_Test()
public void All_Smoke_Test()
{ {
// Arrange // Arrange
using var stdin = new StringReader("hello world"); using var stdin = new StringReader("hello world");

View File

@@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Changelog.md = Changelog.md Changelog.md = Changelog.md
License.txt = License.txt License.txt = License.txt
Readme.md = Readme.md Readme.md = Readme.md
CliFx.props = CliFx.props
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace CliFx.Models namespace CliFx
{ {
/// <summary> /// <summary>
/// Configuration of an application. /// Configuration of an application.
@@ -26,7 +26,8 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="ApplicationConfiguration"/>. /// Initializes an instance of <see cref="ApplicationConfiguration"/>.
/// </summary> /// </summary>
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes, public ApplicationConfiguration(
IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed, bool isPreviewModeAllowed) bool isDebugModeAllowed, bool isPreviewModeAllowed)
{ {
CommandTypes = commandTypes; CommandTypes = commandTypes;

View File

@@ -1,4 +1,4 @@
namespace CliFx.Models namespace CliFx
{ {
/// <summary> /// <summary>
/// Metadata associated with an application. /// Metadata associated with an application.

View File

@@ -1,42 +0,0 @@
using System;
namespace CliFx.Attributes
{
/// <summary>
/// Annotates a property that defines a command argument.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandArgumentAttribute : Attribute
{
/// <summary>
/// The name of the argument, which is used in help text.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Whether the argument is required.
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Argument description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The ordering of the argument. Lower values will appear before higher values.
/// <remarks>
/// Two arguments of the same command cannot have the same <see cref="Order"/>.
/// </remarks>
/// </summary>
public int Order { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandArgumentAttribute"/> with a given order.
/// </summary>
public CommandArgumentAttribute(int order)
{
Order = order;
}
}
}

View File

@@ -10,7 +10,9 @@ namespace CliFx.Attributes
{ {
/// <summary> /// <summary>
/// Command name. /// Command name.
/// This can be null if this is the default command. /// If the name is not set, the command is treated as a default command, i.e. the one that gets executed when the user
/// does not specify a command name in the arguments.
/// All commands in an application must have different names. Likewise, only one command without a name is allowed.
/// </summary> /// </summary>
public string? Name { get; } public string? Name { get; }

View File

@@ -11,12 +11,14 @@ namespace CliFx.Attributes
/// <summary> /// <summary>
/// Option name. /// Option name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set. /// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have different names (comparison is not case-sensitive).
/// </summary> /// </summary>
public string? Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// Option short name. /// Option short name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set. /// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have different short names (comparison is case-sensitive).
/// </summary> /// </summary>
public char? ShortName { get; } public char? ShortName { get; }
@@ -31,7 +33,7 @@ namespace CliFx.Attributes
public string? Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified. /// Environment variable that will be used as fallback if no option value is specified.
/// </summary> /// </summary>
public string? EnvironmentVariableName { get; set; } public string? EnvironmentVariableName { get; set; }

View File

@@ -0,0 +1,37 @@
using System;
namespace CliFx.Attributes
{
/// <summary>
/// Annotates a property that defines a command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandParameterAttribute : Attribute
{
/// <summary>
/// Order of this parameter compared to other parameters.
/// All parameters in a command must have different order.
/// Parameter whose type is a non-scalar (e.g. array), must be the last in order and only one such parameter is allowed.
/// </summary>
public int Order { get; }
/// <summary>
/// Parameter name, which is only used in help text.
/// If this isn't specified, property name is used instead.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Parameter description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
/// </summary>
public CommandParameterAttribute(int order)
{
Order = order;
}
}
}

View File

@@ -0,0 +1,314 @@
using System;
using System.Linq;
using CliFx.Domain;
using CliFx.Internal;
namespace CliFx
{
public partial class CliApplication
{
private void RenderHelp(ApplicationSchema applicationSchema, CommandSchema command)
{
var column = 0;
var row = 0;
var childCommands = applicationSchema.GetChildCommands(command.Name);
bool IsEmpty() => column == 0 && row == 0;
void Render(string text)
{
_console.Output.Write(text);
column += text.Length;
}
void RenderNewLine()
{
_console.Output.WriteLine();
column = 0;
row++;
}
void RenderMargin(int lines = 1)
{
if (!IsEmpty())
{
for (var i = 0; i < lines; i++)
RenderNewLine();
}
}
void RenderIndent(int spaces = 2)
{
Render(' '.Repeat(spaces));
}
void RenderColumnIndent(int spaces = 20, int margin = 2)
{
if (column + margin >= spaces)
{
RenderNewLine();
RenderIndent(spaces);
}
else
{
RenderIndent(spaces - column);
}
}
void RenderWithColor(string text, ConsoleColor foregroundColor)
{
_console.WithForegroundColor(foregroundColor, () => Render(text));
}
void RenderWithColors(string text, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
{
_console.WithColors(foregroundColor, backgroundColor, () => Render(text));
}
void RenderHeader(string text)
{
RenderWithColors(text, ConsoleColor.Black, ConsoleColor.DarkGray);
RenderNewLine();
}
void RenderApplicationInfo()
{
if (!command.IsDefault)
return;
// Title and version
RenderWithColor(_metadata.Title, ConsoleColor.Yellow);
Render(" ");
RenderWithColor(_metadata.VersionText, ConsoleColor.Yellow);
RenderNewLine();
// Description
if (!string.IsNullOrWhiteSpace(_metadata.Description))
{
Render(_metadata.Description);
RenderNewLine();
}
}
void RenderDescription()
{
if (string.IsNullOrWhiteSpace(command.Description))
return;
RenderMargin();
RenderHeader("Description");
RenderIndent();
Render(command.Description);
RenderNewLine();
}
void RenderUsage()
{
RenderMargin();
RenderHeader("Usage");
// Exe name
RenderIndent();
Render(_metadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(command.Name))
{
Render(" ");
RenderWithColor(command.Name, ConsoleColor.Cyan);
}
// Child command placeholder
if (childCommands.Any())
{
Render(" ");
RenderWithColor("[command]", ConsoleColor.Cyan);
}
// Parameters
foreach (var parameter in command.Parameters)
{
Render(" ");
Render($"<{parameter.DisplayName}>");
}
// Required options
var requiredOptionSchemas = command.Options
.Where(o => o.IsRequired)
.ToArray();
foreach (var option in requiredOptionSchemas)
{
Render(" ");
if (!string.IsNullOrWhiteSpace(option.Name))
{
RenderWithColor($"--{option.Name}", ConsoleColor.White);
Render(" ");
Render("<value>");
}
else
{
RenderWithColor($"-{option.ShortName} <value>", ConsoleColor.White);
Render(" ");
Render("<value>");
}
}
// Options placeholder
if (command.Options.Count != requiredOptionSchemas.Length)
{
Render(" ");
RenderWithColor("[options]", ConsoleColor.White);
}
RenderNewLine();
}
void RenderParameters()
{
if (!command.Parameters.Any())
return;
RenderMargin();
RenderHeader("Parameters");
var parameters = command.Parameters
.OrderBy(p => p.Order)
.ToArray();
foreach (var parameter in parameters)
{
RenderWithColor("* ", ConsoleColor.Red);
RenderWithColor($"{parameter.DisplayName}", ConsoleColor.White);
// Description
if (!string.IsNullOrWhiteSpace(parameter.Description))
{
RenderColumnIndent();
Render(parameter.Description);
}
RenderNewLine();
}
}
void RenderOptions()
{
RenderMargin();
RenderHeader("Options");
var options = command.Options
.OrderByDescending(o => o.IsRequired)
.ToList();
// Add built-in options
options.Add(CommandOptionSchema.HelpOption);
if (command.IsDefault)
options.Add(CommandOptionSchema.VersionOption);
foreach (var option in options)
{
if (option.IsRequired)
{
RenderWithColor("* ", ConsoleColor.Red);
}
else
{
RenderIndent();
}
// Short name
if (option.ShortName != null)
{
RenderWithColor($"-{option.ShortName}", ConsoleColor.White);
}
// Delimiter
if (!string.IsNullOrWhiteSpace(option.Name) && option.ShortName != null)
{
Render("|");
}
// Name
if (!string.IsNullOrWhiteSpace(option.Name))
{
RenderWithColor($"--{option.Name}", ConsoleColor.White);
}
// Description
if (!string.IsNullOrWhiteSpace(option.Description))
{
RenderColumnIndent();
Render(option.Description);
}
RenderNewLine();
}
}
void RenderChildCommands()
{
if (!childCommands.Any())
return;
RenderMargin();
RenderHeader("Commands");
foreach (var childCommand in childCommands)
{
var relativeCommandName =
string.IsNullOrWhiteSpace(childCommand.Name) || string.IsNullOrWhiteSpace(command.Name)
? childCommand.Name
: childCommand.Name.Substring(command.Name.Length + 1);
// Name
RenderIndent();
RenderWithColor(relativeCommandName, ConsoleColor.Cyan);
// Description
if (!string.IsNullOrWhiteSpace(childCommand.Description))
{
RenderColumnIndent();
Render(childCommand.Description);
}
RenderNewLine();
}
RenderMargin();
// Child command help tip
Render("You can run `");
Render(_metadata.ExecutableName);
if (!string.IsNullOrWhiteSpace(command.Name))
{
Render(" ");
RenderWithColor(command.Name, ConsoleColor.Cyan);
}
Render(" ");
RenderWithColor("[command]", ConsoleColor.Cyan);
Render(" ");
RenderWithColor("--help", ConsoleColor.White);
Render("` to show help on a specific command.");
RenderNewLine();
}
_console.ResetColor();
RenderApplicationInfo();
RenderDescription();
RenderUsage();
RenderParameters();
RenderOptions();
RenderChildCommands();
}
}
}

View File

@@ -1,83 +1,65 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplication"/>. /// Command line application facade.
/// </summary> /// </summary>
public class CliApplication : ICliApplication public partial class CliApplication
{ {
private readonly ApplicationMetadata _metadata; private readonly ApplicationMetadata _metadata;
private readonly ApplicationConfiguration _configuration; private readonly ApplicationConfiguration _configuration;
private readonly IConsole _console; private readonly IConsole _console;
private readonly ICommandInputParser _commandInputParser; private readonly ITypeActivator _typeActivator;
private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandFactory _commandFactory;
private readonly ICommandInitializer _commandInitializer;
private readonly IHelpTextRenderer _helpTextRenderer;
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CliApplication"/>. /// Initializes an instance of <see cref="CliApplication"/>.
/// </summary> /// </summary>
public CliApplication(ApplicationMetadata metadata, ApplicationConfiguration configuration, public CliApplication(
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver, ApplicationMetadata metadata, ApplicationConfiguration configuration,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer) IConsole console, ITypeActivator typeActivator)
{ {
_metadata = metadata; _metadata = metadata;
_configuration = configuration; _configuration = configuration;
_console = console; _console = console;
_commandInputParser = commandInputParser; _typeActivator = typeActivator;
_commandSchemaResolver = commandSchemaResolver;
_commandFactory = commandFactory;
_commandInitializer = commandInitializer;
_helpTextRenderer = helpTextRenderer;
} }
private async ValueTask<int?> HandleDebugDirectiveAsync(CommandInput commandInput) private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
{ {
// Debug mode is enabled if it's allowed in the application and it was requested via corresponding directive var isDebugMode = _configuration.IsDebugModeAllowed && commandLineInput.IsDebugDirectiveSpecified;
var isDebugMode = _configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified();
// If not in debug mode, pass execution to the next handler
if (!isDebugMode) if (!isDebugMode)
return null; return null;
// Inform user which process they need to attach debugger to _console.WithForegroundColor(ConsoleColor.Green, () =>
_console.WithForegroundColor(ConsoleColor.Green, _console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
() => _console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
// Wait until debugger is attached
while (!Debugger.IsAttached) while (!Debugger.IsAttached)
await Task.Delay(100); await Task.Delay(100);
// Debug directive never short-circuits
return null; return null;
} }
private int? HandlePreviewDirective(CommandInput commandInput) private int? HandlePreviewDirective(CommandLineInput commandLineInput)
{ {
// Preview mode is enabled if it's allowed in the application and it was requested via corresponding directive var isPreviewMode = _configuration.IsPreviewModeAllowed && commandLineInput.IsPreviewDirectiveSpecified;
var isPreviewMode = _configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified();
// If not in preview mode, pass execution to the next handler
if (!isPreviewMode) if (!isPreviewMode)
return null; return null;
// Render command name // Render command name
_console.Output.WriteLine($"Arguments: {string.Join(" ", commandInput.Arguments)}"); _console.Output.WriteLine($"Arguments: {string.Join(" ", commandLineInput.Arguments)}");
_console.Output.WriteLine(); _console.Output.WriteLine();
// Render directives // Render directives
_console.Output.WriteLine("Directives:"); _console.Output.WriteLine("Directives:");
foreach (var directive in commandInput.Directives) foreach (var directive in commandLineInput.Directives)
{ {
_console.Output.Write(" "); _console.Output.Write(" ");
_console.Output.WriteLine(directive); _console.Output.WriteLine(directive);
@@ -88,110 +70,79 @@ namespace CliFx
// Render options // Render options
_console.Output.WriteLine("Options:"); _console.Output.WriteLine("Options:");
foreach (var option in commandInput.Options) foreach (var option in commandLineInput.Options)
{ {
_console.Output.Write(" "); _console.Output.Write(" ");
_console.Output.WriteLine(option); _console.Output.WriteLine(option);
} }
// Short-circuit with exit code 0
return 0; return 0;
} }
private int? HandleVersionOption(CommandInput commandInput) private int? HandleVersionOption(CommandLineInput commandLineInput)
{ {
// Version should be rendered if it was requested on a default command // Version option is available only on the default command (i.e. when arguments are not specified)
var shouldRenderVersion = !commandInput.HasArguments() && commandInput.IsVersionOptionSpecified(); var shouldRenderVersion = !commandLineInput.Arguments.Any() && commandLineInput.IsVersionOptionSpecified;
// If shouldn't render version, pass execution to the next handler
if (!shouldRenderVersion) if (!shouldRenderVersion)
return null; return null;
// Render version text
_console.Output.WriteLine(_metadata.VersionText); _console.Output.WriteLine(_metadata.VersionText);
// Short-circuit with exit code 0
return 0; return 0;
} }
private int? HandleHelpOption(CommandInput commandInput, private int? HandleHelpOption(
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandCandidate? commandCandidate) ApplicationSchema applicationSchema,
CommandLineInput commandLineInput)
{ {
// Help should be rendered if it was requested, or when executing a command which isn't defined // Help is rendered either when it's requested or when the user provides no arguments and there is no default command
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || commandCandidate == null; var shouldRenderHelp =
commandLineInput.IsHelpOptionSpecified ||
!applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.Arguments.Any() && !commandLineInput.Options.Any();
// If shouldn't render help, pass execution to the next handler
if (!shouldRenderHelp) if (!shouldRenderHelp)
return null; return null;
// Keep track whether there was an error in the input // Get the command schema that matches the input or use a dummy default command as a fallback
var isError = false; var commandSchema =
applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand;
// Report error if no command matched the arguments RenderHelp(applicationSchema, commandSchema);
if (commandCandidate is null)
{
// If a command was specified, inform the user that the command is not defined
if (commandInput.HasArguments())
{
_console.WithForegroundColor(ConsoleColor.Red,
() => _console.Error.WriteLine($"No command could be matched for input [{string.Join(" ", commandInput.Arguments)}]"));
isError = true;
}
commandCandidate = new CommandCandidate(CommandSchema.StubDefaultCommand, new string[0], commandInput);
}
// Build help text source
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, commandCandidate.Schema);
// Render help text
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
// Short-circuit with appropriate exit code
return isError ? -1 : 0;
}
private async ValueTask<int> HandleCommandExecutionAsync(CommandCandidate? commandCandidate)
{
if (commandCandidate is null)
{
throw new ArgumentException("Cannot execute command because it was not found.");
}
// Create an instance of the command
var command = _commandFactory.CreateCommand(commandCandidate.Schema);
// Populate command with options and arguments according to its schema
_commandInitializer.InitializeCommand(command, commandCandidate);
// Execute command
await command.ExecuteAsync(_console);
// Finish the chain with exit code 0
return 0; return 0;
} }
/// <inheritdoc /> private async ValueTask<int> HandleCommandExecutionAsync(
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments) ApplicationSchema applicationSchema,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables)
{
await applicationSchema
.InitializeEntryPoint(commandLineInput, environmentVariables, _typeActivator)
.ExecuteAsync(_console);
return 0;
}
/// <summary>
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
/// </summary>
public async ValueTask<int> RunAsync(
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables)
{ {
try try
{ {
// Parse command input from arguments var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments); var commandLineInput = CommandLineInput.Parse(commandLineArguments);
// Get schemas for all available command types
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
// Find command schema matching the name specified in the input
var commandCandidate = _commandSchemaResolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Chain handlers until the first one that produces an exit code
return return
await HandleDebugDirectiveAsync(commandInput) ?? await HandleDebugDirectiveAsync(commandLineInput) ??
HandlePreviewDirective(commandInput) ?? HandlePreviewDirective(commandLineInput) ??
HandleVersionOption(commandInput) ?? HandleVersionOption(commandLineInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, commandCandidate) ?? HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(commandCandidate); await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -199,25 +150,42 @@ namespace CliFx
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions. // Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
// Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException // Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
if (!string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)) var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
{ ? ex.Message
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message)); : ex.ToString();
}
else
{
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex));
}
// Return exit code if it was specified via CommandException _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
if (ex is CommandException commandException)
{ return ex is CommandException commandException
return commandException.ExitCode; ? commandException.ExitCode
} : ex.HResult;
else
{
return ex.HResult;
}
} }
} }
/// <summary>
/// Runs the application with specified command line arguments and returns the exit code.
/// Environment variables are retrieved automatically.
/// </summary>
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{
var environmentVariables = Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.OrdinalIgnoreCase);
return await RunAsync(commandLineArguments, environmentVariables);
}
/// <summary>
/// Runs the application and returns the exit code.
/// Command line arguments and environment variables are retrieved automatically.
/// </summary>
public async ValueTask<int> RunAsync()
{
var commandLineArguments = Environment.GetCommandLineArgs()
.Skip(1)
.ToArray();
return await RunAsync(commandLineArguments);
}
} }
} }

View File

@@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using CliFx.Attributes; using CliFx.Domain;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplicationBuilder"/>. /// Builds an instance of <see cref="CliApplication"/>.
/// </summary> /// </summary>
public partial class CliApplicationBuilder : ICliApplicationBuilder public partial class CliApplicationBuilder
{ {
private readonly HashSet<Type> _commandTypes = new HashSet<Type>(); private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
@@ -24,121 +21,153 @@ namespace CliFx
private string? _versionText; private string? _versionText;
private string? _description; private string? _description;
private IConsole? _console; private IConsole? _console;
private ICommandFactory? _commandFactory; private ITypeActivator? _typeActivator;
private ICommandInputConverter? _commandInputConverter;
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AddCommand(Type commandType) /// Adds a command of specified type to the application.
/// </summary>
public CliApplicationBuilder AddCommand(Type commandType)
{ {
_commandTypes.Add(commandType); _commandTypes.Add(commandType);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) /// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
{ {
var commandTypes = commandAssembly.ExportedTypes
.Where(t => t.Implements(typeof(ICommand)))
.Where(t => t.IsDefined(typeof(CommandAttribute)))
.Where(t => !t.IsAbstract && !t.IsInterface);
foreach (var commandType in commandTypes) foreach (var commandType in commandTypes)
AddCommand(commandType); AddCommand(commandType);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AllowDebugMode(bool isAllowed = true) /// Adds commands from the specified assembly to the application.
/// Only the public types are added.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{
foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType))
AddCommand(commandType);
return this;
}
/// <summary>
/// Adds commands from the specified assemblies to the application.
/// Only the public types are added.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
{
foreach (var commandAssembly in commandAssemblies)
AddCommandsFrom(commandAssembly);
return this;
}
/// <summary>
/// Adds commands from the calling assembly to the application.
/// Only the public types are added.
/// </summary>
public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly());
/// <summary>
/// Specifies whether debug mode (enabled with [debug] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowDebugMode(bool isAllowed = true)
{ {
_isDebugModeAllowed = isAllowed; _isDebugModeAllowed = isAllowed;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true) /// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
{ {
_isPreviewModeAllowed = isAllowed; _isPreviewModeAllowed = isAllowed;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseTitle(string title) /// Sets application title, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseTitle(string title)
{ {
_title = title; _title = title;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseExecutableName(string executableName) /// Sets application executable name, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseExecutableName(string executableName)
{ {
_executableName = executableName; _executableName = executableName;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseVersionText(string versionText) /// Sets application version text, which appears in the help text and when the user requests version information.
/// </summary>
public CliApplicationBuilder UseVersionText(string versionText)
{ {
_versionText = versionText; _versionText = versionText;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseDescription(string? description) /// Sets application description, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseDescription(string? description)
{ {
_description = description; _description = description;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseConsole(IConsole console) /// Configures the application to use the specified implementation of <see cref="IConsole"/>.
/// </summary>
public CliApplicationBuilder UseConsole(IConsole console)
{ {
_console = console; _console = console;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory) /// Configures the application to use the specified implementation of <see cref="ITypeActivator"/>.
/// </summary>
public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator)
{ {
_commandFactory = factory; _typeActivator = typeActivator;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter) /// Configures the application to use the specified function for activating types.
{ /// </summary>
_commandInputConverter = converter; public CliApplicationBuilder UseTypeActivator(Func<Type, object> typeActivator) =>
return this; UseTypeActivator(new DelegateTypeActivator(typeActivator));
}
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider) /// Creates an instance of <see cref="CliApplication"/> using configured parameters.
/// Default values are used in place of parameters that were not specified.
/// </summary>
public CliApplication Build()
{ {
_environmentVariablesProvider = environmentVariablesProvider;
return this;
}
/// <inheritdoc />
public ICliApplication Build()
{
// Use defaults for required parameters that were not configured
_title ??= GetDefaultTitle() ?? "App"; _title ??= GetDefaultTitle() ?? "App";
_executableName ??= GetDefaultExecutableName() ?? "app"; _executableName ??= GetDefaultExecutableName() ?? "app";
_versionText ??= GetDefaultVersionText() ?? "v1.0"; _versionText ??= GetDefaultVersionText() ?? "v1.0";
_console ??= new SystemConsole(); _console ??= new SystemConsole();
_commandFactory ??= new CommandFactory(); _typeActivator ??= new DefaultTypeActivator();
_commandInputConverter ??= new CommandInputConverter();
_environmentVariablesProvider ??= new EnvironmentVariablesProvider();
// Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description); var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed); var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration, return new CliApplication(metadata, configuration, _console, _typeActivator);
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(new CommandArgumentSchemasValidator()),
_commandFactory, new CommandInitializer(_commandInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
} }
} }
@@ -149,9 +178,9 @@ namespace CliFx
// Entry assembly is null in tests // Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value; private static Assembly EntryAssembly => LazyEntryAssembly.Value;
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name ?? ""; private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
private static string GetDefaultExecutableName() private static string? GetDefaultExecutableName()
{ {
var entryAssemblyLocation = EntryAssembly?.Location; var entryAssemblyLocation = EntryAssembly?.Location;
@@ -165,6 +194,9 @@ namespace CliFx
return Path.GetFileNameWithoutExtension(entryAssemblyLocation); return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
} }
private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : ""; private static string? GetDefaultVersionText() =>
EntryAssembly != null
? $"v{EntryAssembly.GetName().Version}"
: null;
} }
} }

View File

@@ -18,6 +18,12 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" /> <None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup> </ItemGroup>
@@ -25,7 +31,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.1.1" PrivateAssets="all" /> <PackageReference Include="Nullable" Version="1.2.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net45'"> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net45'">

View File

@@ -0,0 +1,29 @@
using System;
using System.Text;
using CliFx.Exceptions;
namespace CliFx
{
/// <summary>
/// Type activator that uses the <see cref="Activator"/> class to instantiate objects.
/// </summary>
public class DefaultTypeActivator : ITypeActivator
{
/// <inheritdoc />
public object CreateInstance(Type type)
{
try
{
return Activator.CreateInstance(type);
}
catch (Exception ex)
{
throw new CliFxException(new StringBuilder()
.Append($"Failed to create an instance of {type.FullName}.").Append(" ")
.AppendLine("The type must have a public parameter-less constructor in order to be instantiated by the default activator.")
.Append($"To supply a custom activator (for example when using dependency injection), call {nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...).")
.ToString(), ex);
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
namespace CliFx
{
/// <summary>
/// Type activator that uses the specified delegate to instantiate objects.
/// </summary>
public class DelegateTypeActivator : ITypeActivator
{
private readonly Func<Type, object> _func;
/// <summary>
/// Initializes an instance of <see cref="DelegateTypeActivator"/>.
/// </summary>
public DelegateTypeActivator(Func<Type, object> func) => _func = func;
/// <inheritdoc />
public object CreateInstance(Type type) => _func(type);
}
}

View File

@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
namespace CliFx.Domain
{
internal partial class ApplicationSchema
{
public IReadOnlyList<CommandSchema> Commands { get; }
public ApplicationSchema(IReadOnlyList<CommandSchema> commands)
{
Commands = commands;
}
public CommandSchema? TryFindParentCommand(string? childCommandName)
{
// Default command has no parent
if (string.IsNullOrWhiteSpace(childCommandName))
return null;
// Try to find the parent command by repeatedly biting off chunks of its name
var route = childCommandName.Split(' ');
for (var i = route.Length - 1; i >= 1; i--)
{
var potentialParentCommandName = string.Join(" ", route.Take(i));
var matchingParentCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialParentCommandName));
if (matchingParentCommand != null)
return matchingParentCommand;
}
// If there's no parent - fall back to default command
return Commands.FirstOrDefault(c => c.IsDefault);
}
public IReadOnlyList<CommandSchema> GetChildCommands(string? parentCommandName) =>
!string.IsNullOrWhiteSpace(parentCommandName) || Commands.Any(c => c.IsDefault)
? Commands.Where(c => TryFindParentCommand(c.Name)?.MatchesName(parentCommandName) == true).ToArray()
: Commands.Where(c => !string.IsNullOrWhiteSpace(c.Name) && TryFindParentCommand(c.Name) == null).ToArray();
private CommandSchema? TryFindCommand(CommandLineInput commandLineInput, out int argumentOffset)
{
// Try to find the command that contains the most of the input arguments in its name
for (var i = commandLineInput.Arguments.Count; i >= 0; i--)
{
var potentialCommandName = string.Join(" ", commandLineInput.Arguments.Take(i));
var matchingCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialCommandName));
if (matchingCommand != null)
{
argumentOffset = i;
return matchingCommand;
}
}
argumentOffset = 0;
return Commands.FirstOrDefault(c => c.IsDefault);
}
public CommandSchema? TryFindCommand(CommandLineInput commandLineInput) =>
TryFindCommand(commandLineInput, out _);
public ICommand InitializeEntryPoint(
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables,
ITypeActivator activator)
{
var command = TryFindCommand(commandLineInput, out var argumentOffset);
if (command == null)
{
throw new CliFxException(
$"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.Arguments)}].");
}
var parameterInputs = argumentOffset == 0
? commandLineInput.Arguments
: commandLineInput.Arguments.Skip(argumentOffset).ToArray();
return command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator);
}
}
internal partial class ApplicationSchema
{
private static void ValidateParameters(CommandSchema command)
{
var duplicateOrderGroup = command.Parameters
.GroupBy(a => a.Order)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateOrderGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same order ({duplicateOrderGroup.Key}):")
.AppendBulletList(duplicateOrderGroup.Select(o => o.Property.Name))
.AppendLine()
.Append("Parameters in a command must all have unique order.")
.ToString());
}
var duplicateNameGroup = command.Parameters
.Where(a => !string.IsNullOrWhiteSpace(a.Name))
.GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateNameGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same name ({duplicateNameGroup.Key}):")
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name))
.AppendLine()
.Append("Parameters in a command must all have unique names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
}
var nonScalarParameters = command.Parameters
.Where(p => !p.IsScalar)
.ToArray();
if (nonScalarParameters.Length > 1)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command [{command.Type.FullName}] contains two or more parameters of an enumerable type:")
.AppendBulletList(nonScalarParameters.Select(o => o.Property.Name))
.AppendLine()
.AppendLine("There can only be one parameter of an enumerable type in a command.")
.Append("Note, the string type is not considered enumerable in this context.")
.ToString());
}
var nonLastNonScalarParameter = command.Parameters
.OrderByDescending(a => a.Order)
.Skip(1)
.LastOrDefault(p => !p.IsScalar);
if (nonLastNonScalarParameter != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains a parameter of an enumerable type which doesn't appear last in order:")
.AppendLine($"- {nonLastNonScalarParameter.Property.Name}")
.AppendLine()
.Append("Parameter of an enumerable type must always come last to avoid ambiguity.")
.ToString());
}
}
private static void ValidateOptions(CommandSchema command)
{
var duplicateNameGroup = command.Options
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
.GroupBy(o => o.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateNameGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same name ({duplicateNameGroup.Key}):")
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name))
.AppendLine()
.Append("Options in a command must all have unique names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
}
var duplicateShortNameGroup = command.Options
.Where(o => o.ShortName != null)
.GroupBy(o => o.ShortName)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateShortNameGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same short name ({duplicateShortNameGroup.Key}):")
.AppendBulletList(duplicateShortNameGroup.Select(o => o.Property.Name))
.AppendLine()
.Append("Options in a command must all have unique short names.").Append(" ")
.Append("Comparison is case-sensitive.")
.ToString());
}
var duplicateEnvironmentVariableNameGroup = command.Options
.Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName))
.GroupBy(o => o.EnvironmentVariableName, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateEnvironmentVariableNameGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same environment variable name ({duplicateEnvironmentVariableNameGroup.Key}):")
.AppendBulletList(duplicateEnvironmentVariableNameGroup.Select(o => o.Property.Name))
.AppendLine()
.Append("Options in a command must all have unique environment variable names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
}
}
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
{
if (!commands.Any())
{
throw new CliFxException("There are no commands configured for this application.");
}
var duplicateNameGroup = commands
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1);
if (duplicateNameGroup != null)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Application contains two or more commands that have the same name ({duplicateNameGroup.Key}):")
.AppendBulletList(duplicateNameGroup.Select(o => o.Type.FullName))
.AppendLine()
.Append("Commands must all have unique names. Likewise, there must not be more than one command without a name.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
}
}
public static ApplicationSchema Resolve(IReadOnlyList<Type> commandTypes)
{
var commands = new List<CommandSchema>();
foreach (var commandType in commandTypes)
{
var command = CommandSchema.TryResolve(commandType);
if (command == null)
{
throw new CliFxException(new StringBuilder()
.Append($"Command {commandType.FullName} is not a valid command type.").Append(" ")
.AppendLine("In order to be a valid command type it must:")
.AppendLine($" - Be annotated with {typeof(CommandAttribute).FullName}")
.AppendLine($" - Implement {typeof(ICommand).FullName}")
.AppendLine(" - Not be an abstract class")
.ToString());
}
ValidateParameters(command);
ValidateOptions(command);
commands.Add(command);
}
ValidateCommands(commands);
return new ApplicationSchema(commands);
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using CliFx.Exceptions;
using CliFx.Internal;
namespace CliFx.Domain
{
internal abstract partial class CommandArgumentSchema
{
public PropertyInfo Property { get; }
public string? Description { get; }
public bool IsScalar => GetEnumerableArgumentUnderlyingType() == null;
protected CommandArgumentSchema(PropertyInfo property, string? description)
{
Property = property;
Description = description;
}
private Type? GetEnumerableArgumentUnderlyingType() =>
Property.PropertyType != typeof(string)
? Property.PropertyType.GetEnumerableUnderlyingType()
: null;
private object Convert(IReadOnlyList<string> values)
{
var targetType = Property.PropertyType;
var enumerableUnderlyingType = GetEnumerableArgumentUnderlyingType();
// Scalar
if (enumerableUnderlyingType == null)
{
if (values.Count > 1)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Can't convert a sequence of values [{string.Join(", ", values)}] to type {targetType.FullName}.")
.Append("Target type is not enumerable and can't accept more than one value.")
.ToString());
}
return ConvertScalar(values.SingleOrDefault(), targetType);
}
// Non-scalar
else
{
return ConvertNonScalar(values, targetType, enumerableUnderlyingType);
}
}
public void Inject(ICommand command, IReadOnlyList<string> values) =>
Property.SetValue(command, Convert(values));
public void Inject(ICommand command, params string[] values) =>
Inject(command, (IReadOnlyList<string>) values);
}
internal partial class CommandArgumentSchema
{
private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture;
private static readonly IReadOnlyDictionary<Type, Func<string, object>> PrimitiveConverters =
new Dictionary<Type, Func<string, object>>
{
[typeof(object)] = v => v,
[typeof(string)] = v => v,
[typeof(bool)] = v => string.IsNullOrWhiteSpace(v) || bool.Parse(v),
[typeof(char)] = v => v.Single(),
[typeof(sbyte)] = v => sbyte.Parse(v, ConversionFormatProvider),
[typeof(byte)] = v => byte.Parse(v, ConversionFormatProvider),
[typeof(short)] = v => short.Parse(v, ConversionFormatProvider),
[typeof(ushort)] = v => ushort.Parse(v, ConversionFormatProvider),
[typeof(int)] = v => int.Parse(v, ConversionFormatProvider),
[typeof(uint)] = v => uint.Parse(v, ConversionFormatProvider),
[typeof(long)] = v => long.Parse(v, ConversionFormatProvider),
[typeof(ulong)] = v => ulong.Parse(v, ConversionFormatProvider),
[typeof(float)] = v => float.Parse(v, ConversionFormatProvider),
[typeof(double)] = v => double.Parse(v, ConversionFormatProvider),
[typeof(decimal)] = v => decimal.Parse(v, ConversionFormatProvider),
[typeof(DateTime)] = v => DateTime.Parse(v, ConversionFormatProvider),
[typeof(DateTimeOffset)] = v => DateTimeOffset.Parse(v, ConversionFormatProvider),
[typeof(TimeSpan)] = v => TimeSpan.Parse(v, ConversionFormatProvider),
};
private static ConstructorInfo? GetStringConstructor(Type type) =>
type.GetConstructor(new[] {typeof(string)});
private static MethodInfo? GetStaticParseMethod(Type type) =>
type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string)}, null);
private static MethodInfo? GetStaticParseMethodWithFormatProvider(Type type) =>
type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string), typeof(IFormatProvider)}, null);
private static object ConvertScalar(string? value, Type targetType)
{
try
{
// Primitive
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
if (primitiveConverter != null)
return primitiveConverter(value);
// Enum
if (targetType.IsEnum)
return Enum.Parse(targetType, value, true);
// Nullable
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
if (nullableUnderlyingType != null)
return !string.IsNullOrWhiteSpace(value)
? ConvertScalar(value, nullableUnderlyingType)
: null;
// String-constructable
var stringConstructor = GetStringConstructor(targetType);
if (stringConstructor != null)
return stringConstructor.Invoke(new object[] {value});
// String-parseable (with format provider)
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
if (parseMethodWithFormatProvider != null)
return parseMethodWithFormatProvider.Invoke(null, new object[] {value, ConversionFormatProvider});
// String-parseable (without format provider)
var parseMethod = GetStaticParseMethod(targetType);
if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value});
}
catch (Exception ex)
{
throw new CliFxException(new StringBuilder()
.AppendLine($"Failed to convert value '{value ?? "<null>"}' to type {targetType.FullName}.")
.Append(ex.Message)
.ToString(), ex);
}
throw new CliFxException(new StringBuilder()
.AppendLine($"Can't convert value '{value ?? "<null>"}' to type {targetType.FullName}.")
.Append("Target type is not supported by CliFx.")
.ToString());
}
private static object ConvertNonScalar(IReadOnlyList<string> values, Type targetEnumerableType, Type targetElementType)
{
var array = values
.Select(v => ConvertScalar(v, targetElementType))
.ToNonGenericArray(targetElementType);
var arrayType = array.GetType();
// Assignable from an array
if (targetEnumerableType.IsAssignableFrom(arrayType))
return array;
// Constructable from an array
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {array});
throw new CliFxException(new StringBuilder()
.AppendLine($"Can't convert a sequence of values [{string.Join(", ", values)}] to type {targetEnumerableType.FullName}.")
.AppendLine($"Underlying element type is [{targetElementType.FullName}].")
.Append("Target type must either be assignable from an array or have a public constructor that takes a single array argument.")
.ToString());
}
}
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CliFx.Internal;
namespace CliFx.Domain
{
internal partial class CommandLineInput
{
public IReadOnlyList<string> Directives { get; }
public IReadOnlyList<string> Arguments { get; }
public IReadOnlyList<CommandOptionInput> Options { get; }
public bool IsDebugDirectiveSpecified => Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
public bool IsPreviewDirectiveSpecified => Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
public bool IsHelpOptionSpecified =>
Options.Any(o => CommandOptionSchema.HelpOption.MatchesNameOrShortName(o.Alias));
public bool IsVersionOptionSpecified =>
Options.Any(o => CommandOptionSchema.VersionOption.MatchesNameOrShortName(o.Alias));
public CommandLineInput(
IReadOnlyList<string> directives,
IReadOnlyList<string> arguments,
IReadOnlyList<CommandOptionInput> options)
{
Directives = directives;
Arguments = arguments;
Options = options;
}
public CommandLineInput(
IReadOnlyList<string> arguments,
IReadOnlyList<CommandOptionInput> options)
: this(new string[0], arguments, options)
{
}
public CommandLineInput(IReadOnlyList<string> arguments)
: this(arguments, new CommandOptionInput[0])
{
}
public CommandLineInput(IReadOnlyList<CommandOptionInput> options)
: this(new string[0], options)
{
}
public override string ToString()
{
var buffer = new StringBuilder();
foreach (var directive in Directives)
{
buffer.AppendIfNotEmpty(' ');
buffer
.Append('[')
.Append(directive)
.Append(']');
}
foreach (var argument in Arguments)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(argument);
}
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(option);
}
return buffer.ToString();
}
}
internal partial class CommandLineInput
{
public static CommandLineInput Parse(IReadOnlyList<string> commandLineArguments)
{
var directives = new List<string>();
var arguments = new List<string>();
var optionsDic = new Dictionary<string, List<string>>();
// Option aliases and values are parsed in pairs so we need to keep track of last alias
var lastOptionAlias = "";
bool TryParseDirective(string argument)
{
if (!string.IsNullOrWhiteSpace(lastOptionAlias))
return false;
if (!argument.StartsWith("[", StringComparison.OrdinalIgnoreCase) ||
!argument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
return false;
var directive = argument.Substring(1, argument.Length - 2);
directives.Add(directive);
return true;
}
bool TryParseArgument(string argument)
{
if (!string.IsNullOrWhiteSpace(lastOptionAlias))
return false;
arguments.Add(argument);
return true;
}
bool TryParseOptionName(string argument)
{
if (!argument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
return false;
lastOptionAlias = argument.Substring(2);
if (!optionsDic.ContainsKey(lastOptionAlias))
optionsDic[lastOptionAlias] = new List<string>();
return true;
}
bool TryParseOptionShortName(string argument)
{
if (!argument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
return false;
foreach (var c in argument.Substring(1))
{
lastOptionAlias = c.AsString();
if (!optionsDic.ContainsKey(lastOptionAlias))
optionsDic[lastOptionAlias] = new List<string>();
}
return true;
}
bool TryParseOptionValue(string argument)
{
if (string.IsNullOrWhiteSpace(lastOptionAlias))
return false;
optionsDic[lastOptionAlias].Add(argument);
return true;
}
foreach (var argument in commandLineArguments)
{
var _ =
TryParseOptionName(argument) ||
TryParseOptionShortName(argument) ||
TryParseDirective(argument) ||
TryParseArgument(argument) ||
TryParseOptionValue(argument);
}
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
return new CommandLineInput(directives, arguments, options);
}
}
internal partial class CommandLineInput
{
public static CommandLineInput Empty { get; } =
new CommandLineInput(new string[0], new string[0], new CommandOptionInput[0]);
}
}

View File

@@ -2,49 +2,30 @@
using System.Text; using System.Text;
using CliFx.Internal; using CliFx.Internal;
namespace CliFx.Models namespace CliFx.Domain
{ {
/// <summary> internal class CommandOptionInput
/// Parsed option from command line input.
/// </summary>
public partial class CommandOptionInput
{ {
/// <summary>
/// Specified option alias.
/// </summary>
public string Alias { get; } public string Alias { get; }
/// <summary>
/// Specified values.
/// </summary>
public IReadOnlyList<string> Values { get; } public IReadOnlyList<string> Values { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias, IReadOnlyList<string> values) public CommandOptionInput(string alias, IReadOnlyList<string> values)
{ {
Alias = alias; Alias = alias;
Values = values; Values = values;
} }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias, string value) public CommandOptionInput(string alias, string value)
: this(alias, new[] {value}) : this(alias, new[] {value})
{ {
} }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias) public CommandOptionInput(string alias)
: this(alias, EmptyValues) : this(alias, new string[0])
{ {
} }
/// <inheritdoc />
public override string ToString() public override string ToString()
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
@@ -70,9 +51,4 @@ namespace CliFx.Models
return buffer.ToString(); return buffer.ToString();
} }
} }
public partial class CommandOptionInput
{
private static readonly IReadOnlyList<string> EmptyValues = new string[0];
}
} }

View File

@@ -0,0 +1,105 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using CliFx.Attributes;
using CliFx.Internal;
namespace CliFx.Domain
{
internal partial class CommandOptionSchema : CommandArgumentSchema
{
public string? Name { get; }
public char? ShortName { get; }
public string DisplayName => !string.IsNullOrWhiteSpace(Name)
? Name
: ShortName?.AsString()!;
public string? EnvironmentVariableName { get; }
public bool IsRequired { get; }
public CommandOptionSchema(
PropertyInfo property,
string? name,
char? shortName,
string? environmentVariableName,
bool isRequired,
string? description)
: base(property, description)
{
Name = name;
ShortName = shortName;
EnvironmentVariableName = environmentVariableName;
IsRequired = isRequired;
}
public bool MatchesName(string name) =>
!string.IsNullOrWhiteSpace(Name) &&
string.Equals(Name, name, StringComparison.OrdinalIgnoreCase);
public bool MatchesShortName(char shortName) =>
ShortName != null &&
ShortName == shortName;
public bool MatchesNameOrShortName(string alias) =>
MatchesName(alias) ||
alias.Length == 1 && MatchesShortName(alias.Single());
public bool MatchesEnvironmentVariableName(string environmentVariableName) =>
!string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase);
public override string ToString()
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name))
{
buffer.Append("--");
buffer.Append(Name);
}
if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
buffer.Append('|');
if (ShortName != null)
{
buffer.Append('-');
buffer.Append(ShortName);
}
return buffer.ToString();
}
}
internal partial class CommandOptionSchema
{
public static CommandOptionSchema? TryResolve(PropertyInfo property)
{
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
if (attribute == null)
return null;
return new CommandOptionSchema(
property,
attribute.Name,
attribute.ShortName,
attribute.EnvironmentVariableName,
attribute.IsRequired,
attribute.Description
);
}
}
internal partial class CommandOptionSchema
{
public static CommandOptionSchema HelpOption { get; } =
new CommandOptionSchema(null!, "help", 'h', null, false, "Shows help text.");
public static CommandOptionSchema VersionOption { get; } =
new CommandOptionSchema(null!, "version", null, null, false, "Shows version information.");
}
}

View File

@@ -0,0 +1,53 @@
using System.Reflection;
using System.Text;
using CliFx.Attributes;
namespace CliFx.Domain
{
internal partial class CommandParameterSchema : CommandArgumentSchema
{
public int Order { get; }
public string? Name { get; }
public string DisplayName => !string.IsNullOrWhiteSpace(Name)
? Name
: Property.Name.ToUpperInvariant();
public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description)
: base(property, description)
{
Order = order;
Name = name;
}
public override string ToString()
{
var buffer = new StringBuilder();
buffer
.Append('<')
.Append(DisplayName)
.Append('>');
return buffer.ToString();
}
}
internal partial class CommandParameterSchema
{
public static CommandParameterSchema? TryResolve(PropertyInfo property)
{
var attribute = property.GetCustomAttribute<CommandParameterAttribute>();
if (attribute == null)
return null;
return new CommandParameterSchema(
property,
attribute.Order,
attribute.Name,
attribute.Description
);
}
}
}

View File

@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
namespace CliFx.Domain
{
internal partial class CommandSchema
{
public Type Type { get; }
public string? Name { get; }
public bool IsDefault => string.IsNullOrWhiteSpace(Name);
public string? Description { get; }
public IReadOnlyList<CommandParameterSchema> Parameters { get; }
public IReadOnlyList<CommandOptionSchema> Options { get; }
public CommandSchema(
Type type,
string? name,
string? description,
IReadOnlyList<CommandParameterSchema> parameters,
IReadOnlyList<CommandOptionSchema> options)
{
Type = type;
Name = name;
Description = description;
Options = options;
Parameters = parameters;
}
public bool MatchesName(string name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
private void InjectParameters(ICommand command, IReadOnlyList<string> parameterInputs)
{
// Scalar parameters
var scalarParameters = Parameters
.OrderBy(p => p.Order)
.TakeWhile(p => p.IsScalar)
.ToArray();
for (var i = 0; i < scalarParameters.Length; i++)
{
var scalarParameter = scalarParameters[i];
var scalarParameterInput = i < parameterInputs.Count
? parameterInputs[i]
: throw new CliFxException($"Missing value for parameter <{scalarParameter.DisplayName}>.");
scalarParameter.Inject(command, scalarParameterInput);
}
// Non-scalar parameter (only one is allowed)
var nonScalarParameter = Parameters
.OrderBy(p => p.Order)
.FirstOrDefault(p => !p.IsScalar);
if (nonScalarParameter != null)
{
var nonScalarParameterInputs = parameterInputs.Skip(scalarParameters.Length).ToArray();
nonScalarParameter.Inject(command, nonScalarParameterInputs);
}
}
private void InjectOptions(
ICommand command,
IReadOnlyList<CommandOptionInput> optionInputs,
IReadOnlyDictionary<string, string> environmentVariables)
{
// Keep track of required options so that we can raise an error if any of them are not set
var unsetRequiredOptions = Options.Where(o => o.IsRequired).ToList();
// Environment variables
foreach (var environmentVariable in environmentVariables)
{
var option = Options.FirstOrDefault(o => o.MatchesEnvironmentVariableName(environmentVariable.Key));
if (option != null)
{
var values = option.IsScalar
? new[] {environmentVariable.Value}
: environmentVariable.Value.Split(Path.PathSeparator);
option.Inject(command, values);
unsetRequiredOptions.Remove(option);
}
}
// Direct input
foreach (var optionInput in optionInputs)
{
var option = Options.FirstOrDefault(o => o.MatchesNameOrShortName(optionInput.Alias));
if (option != null)
{
option.Inject(command, optionInput.Values);
unsetRequiredOptions.Remove(option);
}
}
if (unsetRequiredOptions.Any())
{
throw new CliFxException(new StringBuilder()
.AppendLine("Missing values for some of the required options:")
.AppendBulletList(unsetRequiredOptions.Select(o => o.DisplayName))
.ToString());
}
}
public ICommand CreateInstance(
IReadOnlyList<string> parameterInputs,
IReadOnlyList<CommandOptionInput> optionInputs,
IReadOnlyDictionary<string, string> environmentVariables,
ITypeActivator activator)
{
var command = (ICommand) activator.CreateInstance(Type);
InjectParameters(command, parameterInputs);
InjectOptions(command, optionInputs, environmentVariables);
return command;
}
public override string ToString()
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
foreach (var parameter in Parameters)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(parameter);
}
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(option);
}
return buffer.ToString();
}
}
internal partial class CommandSchema
{
public static bool IsCommandType(Type type) =>
type.Implements(typeof(ICommand)) &&
type.IsDefined(typeof(CommandAttribute)) &&
!type.IsAbstract &&
!type.IsInterface;
public static CommandSchema? TryResolve(Type type)
{
if (!IsCommandType(type))
return null;
var attribute = type.GetCustomAttribute<CommandAttribute>();
var parameters = type.GetProperties()
.Select(CommandParameterSchema.TryResolve)
.Where(p => p != null)
.ToArray();
var options = type.GetProperties()
.Select(CommandOptionSchema.TryResolve)
.Where(o => o != null)
.ToArray();
return new CommandSchema(
type,
attribute?.Name,
attribute?.Description,
parameters,
options
);
}
}
internal partial class CommandSchema
{
public static CommandSchema StubDefaultCommand { get; } =
new CommandSchema(null!, null, null, new CommandParameterSchema[0], new CommandOptionSchema[0]);
}
}

View File

@@ -22,7 +22,9 @@ namespace CliFx.Exceptions
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode) public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode)
: base(message, innerException) : base(message, innerException)
{ {
ExitCode = exitCode != 0 ? exitCode : throw new ArgumentException("Exit code cannot be zero because that signifies success."); ExitCode = exitCode != 0
? exitCode
: throw new ArgumentException("Exit code must not be zero in order to signify failure.");
} }
/// <summary> /// <summary>

View File

@@ -1,48 +1,42 @@
using System; using System;
using System.Collections.Generic;
using System.Reflection;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Extensions for <see cref="CliFx"/>. /// Extensions for <see cref="CliFx"/>
/// </summary> /// </summary>
public static class Extensions public static class Extensions
{ {
/// <summary> /// <summary>
/// Adds multiple commands to the application. /// Sets console foreground color, executes specified action, and sets the color back to the original value.
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommands(this ICliApplicationBuilder builder, IReadOnlyList<Type> commandTypes) public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
{ {
foreach (var commandType in commandTypes) var lastColor = console.ForegroundColor;
builder.AddCommand(commandType); console.ForegroundColor = foregroundColor;
return builder; action();
console.ForegroundColor = lastColor;
} }
/// <summary> /// <summary>
/// Adds commands from specified assemblies to the application. /// Sets console background color, executes specified action, and sets the color back to the original value.
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommandsFrom(this ICliApplicationBuilder builder, IReadOnlyList<Assembly> commandAssemblies) public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
{ {
foreach (var commandAssembly in commandAssemblies) var lastColor = console.BackgroundColor;
builder.AddCommandsFrom(commandAssembly); console.BackgroundColor = backgroundColor;
return builder; action();
console.BackgroundColor = lastColor;
} }
/// <summary> /// <summary>
/// Adds commands from calling assembly to the application. /// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommandsFromThisAssembly(this ICliApplicationBuilder builder) => public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action) =>
builder.AddCommandsFrom(Assembly.GetCallingAssembly()); console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
/// <summary>
/// Configures application to use specified factory method for creating new instances of <see cref="ICommand"/>.
/// </summary>
public static ICliApplicationBuilder UseCommandFactory(this ICliApplicationBuilder builder, Func<CommandSchema, ICommand> factoryMethod) =>
builder.UseCommandFactory(new DelegateCommandFactory(factoryMethod));
} }
} }

View File

@@ -1,16 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CliFx
{
/// <summary>
/// Entry point for a command line application.
/// </summary>
public interface ICliApplication
{
/// <summary>
/// Runs application with specified command line arguments and returns an exit code.
/// </summary>
ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments);
}
}

View File

@@ -1,78 +0,0 @@
using System;
using System.Reflection;
using CliFx.Services;
namespace CliFx
{
/// <summary>
/// Builds an instance of <see cref="ICliApplication"/>.
/// </summary>
public interface ICliApplicationBuilder
{
/// <summary>
/// Adds a command of specified type to the application.
/// </summary>
ICliApplicationBuilder AddCommand(Type commandType);
/// <summary>
/// Adds commands from specified assembly to the application.
/// </summary>
ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly);
/// <summary>
/// Specifies whether debug mode (enabled with [debug] directive) is allowed in the application.
/// </summary>
ICliApplicationBuilder AllowDebugMode(bool isAllowed = true);
/// <summary>
/// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
/// </summary>
ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true);
/// <summary>
/// Sets application title, which appears in the help text.
/// </summary>
ICliApplicationBuilder UseTitle(string title);
/// <summary>
/// Sets application executable name, which appears in the help text.
/// </summary>
ICliApplicationBuilder UseExecutableName(string executableName);
/// <summary>
/// Sets application version text, which appears in the help text and when the user requests version information.
/// </summary>
ICliApplicationBuilder UseVersionText(string versionText);
/// <summary>
/// Sets application description, which appears in the help text.
/// </summary>
ICliApplicationBuilder UseDescription(string? description);
/// <summary>
/// Configures application to use specified implementation of <see cref="IConsole"/>.
/// </summary>
ICliApplicationBuilder UseConsole(IConsole console);
/// <summary>
/// Configures application to use specified implementation of <see cref="ICommandFactory"/>.
/// </summary>
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
/// <summary>
/// Configures application to use specified implementation of <see cref="ICommandInputConverter"/>.
/// </summary>
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter);
/// <summary>
/// Configures application to use specified implementation of <see cref="IEnvironmentVariablesProvider"/>.
/// </summary>
ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider);
/// <summary>
/// Creates an instance of <see cref="ICliApplication"/> using configured parameters.
/// Default values are used in place of parameters that were not specified.
/// </summary>
ICliApplication Build();
}
}

View File

@@ -1,17 +1,17 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Point of interaction between a user and command line interface. /// Entry point in a command line application.
/// </summary> /// </summary>
public interface ICommand public interface ICommand
{ {
/// <summary> /// <summary>
/// Executes command using specified implementation of <see cref="IConsole"/>. /// Executes the command using the specified implementation of <see cref="IConsole"/>.
/// This method is called when the command is invoked by a user through command line interface. /// This is the method that's called when the command is invoked by a user through command line interface.
/// </summary> /// </summary>
/// <remarks>If the execution of the command is not asynchronous, simply end the method with <code>return default;</code></remarks>
ValueTask ExecuteAsync(IConsole console); ValueTask ExecuteAsync(IConsole console);
} }
} }

View File

@@ -2,7 +2,7 @@
using System.IO; using System.IO;
using System.Threading; using System.Threading;
namespace CliFx.Services namespace CliFx
{ {
/// <summary> /// <summary>
/// Abstraction for interacting with the console. /// Abstraction for interacting with the console.
@@ -55,8 +55,9 @@ namespace CliFx.Services
void ResetColor(); void ResetColor();
/// <summary> /// <summary>
/// Provides token that cancels when application cancellation is requested. /// Provides a token that signals when application cancellation is requested.
/// Subsequent calls return the same token. /// Subsequent calls return the same token.
/// When working with system console, the user can request cancellation by issuing an interrupt signal (Ctrl+C).
/// </summary> /// </summary>
CancellationToken GetCancellationToken(); CancellationToken GetCancellationToken();
} }

15
CliFx/ITypeActivator.cs Normal file
View File

@@ -0,0 +1,15 @@
using System;
namespace CliFx
{
/// <summary>
/// Abstraction for a service can initialize objects at runtime.
/// </summary>
public interface ITypeActivator
{
/// <summary>
/// Creates an instance of specified type.
/// </summary>
object CreateInstance(Type type);
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using CliFx.Models;
namespace CliFx.Internal namespace CliFx.Internal
{ {
@@ -13,24 +12,19 @@ namespace CliFx.Internal
public static string AsString(this char c) => c.Repeat(1); public static string AsString(this char c) => c.Repeat(1);
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
public static string SubstringUntilLast(this string s, string sub,
StringComparison comparison = StringComparison.Ordinal)
{
var index = s.LastIndexOf(sub, comparison);
return index < 0 ? s : s.Substring(0, index);
}
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) => public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
builder.Length > 0 ? builder.Append(value) : builder; builder.Length > 0 ? builder.Append(value) : builder;
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value) public static StringBuilder AppendBulletList<T>(this StringBuilder builder, IEnumerable<T> items)
{ {
foreach (var i in source) foreach (var item in items)
yield return i; {
builder.Append("- ");
builder.Append(item);
builder.AppendLine();
}
yield return value; return builder;
} }
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType); public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
@@ -50,7 +44,7 @@ namespace CliFx.Internal
return type.GetInterfaces() return type.GetInterfaces()
.Select(GetEnumerableUnderlyingType) .Select(GetEnumerableUnderlyingType)
.Where(t => t != default) .Where(t => t != null)
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types .OrderByDescending(t => t != typeof(object)) // prioritize more specific types
.FirstOrDefault(); .FirstOrDefault();
} }
@@ -64,14 +58,5 @@ namespace CliFx.Internal
return array; return array;
} }
public static bool IsCollection(this Type type) =>
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
public static IOrderedEnumerable<CommandArgumentSchema> Ordered(this IEnumerable<CommandArgumentSchema> source)
{
return source
.OrderBy(a => a.Order);
}
} }
} }

View File

@@ -0,0 +1,16 @@
#if NET45 || NETSTANDARD2_0
using System.Collections.Generic;
using System.Text;
namespace CliFx.Internal
{
internal static class Polyfills
{
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> self, TKey key) =>
self.TryGetValue(key, out var value) ? value : default;
public static StringBuilder AppendJoin<T>(this StringBuilder self, string separator, IEnumerable<T> items) =>
self.Append(string.Join(separator, items));
}
}
#endif

View File

@@ -1,78 +0,0 @@
using System.Globalization;
using System.Reflection;
using System.Text;
namespace CliFx.Models
{
/// <summary>
/// Schema of a defined command argument.
/// </summary>
public class CommandArgumentSchema
{
/// <summary>
/// Underlying property.
/// </summary>
public PropertyInfo Property { get; }
/// <summary>
/// Argument name used for help text.
/// </summary>
public string? Name { get; }
/// <summary>
/// Whether the argument is required.
/// </summary>
public bool IsRequired { get; }
/// <summary>
/// Argument description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Order of the argument.
/// </summary>
public int Order { get; }
/// <summary>
/// The display name of the argument. Returns <see cref="Name"/> if specified, otherwise the name of the underlying property.
/// </summary>
public string DisplayName => !string.IsNullOrWhiteSpace(Name) ? Name! : Property.Name.ToLower(CultureInfo.InvariantCulture);
/// <summary>
/// Initializes an instance of <see cref="CommandArgumentSchema"/>.
/// </summary>
public CommandArgumentSchema(PropertyInfo property, string? name, bool isRequired, string? description, int order)
{
Property = property;
Name = name;
IsRequired = isRequired;
Description = description;
Order = order;
}
/// <summary>
/// Returns the string representation of the argument schema.
/// </summary>
/// <returns></returns>
public override string ToString()
{
var sb = new StringBuilder();
if (!IsRequired)
{
sb.Append("[");
}
sb.Append("<");
sb.Append($"{DisplayName}");
sb.Append(">");
if (!IsRequired)
{
sb.Append("]");
}
return sb.ToString();
}
}
}

View File

@@ -1,35 +0,0 @@
using System.Collections.Generic;
namespace CliFx.Models
{
/// <summary>
/// Defines the target command and the input required for initializing the command.
/// </summary>
public class CommandCandidate
{
/// <summary>
/// The command schema of the target command.
/// </summary>
public CommandSchema Schema { get; }
/// <summary>
/// The positional arguments input for the command.
/// </summary>
public IReadOnlyList<string> PositionalArgumentsInput { get; }
/// <summary>
/// The command input for the command.
/// </summary>
public CommandInput CommandInput { get; }
/// <summary>
/// Initializes and instance of <see cref="CommandCandidate"/>
/// </summary>
public CommandCandidate(CommandSchema schema, IReadOnlyList<string> positionalArgumentsInput, CommandInput commandInput)
{
Schema = schema;
PositionalArgumentsInput = positionalArgumentsInput;
CommandInput = commandInput;
}
}
}

View File

@@ -1,122 +0,0 @@
using System.Collections.Generic;
using System.Text;
using CliFx.Internal;
namespace CliFx.Models
{
/// <summary>
/// Parsed command line input.
/// </summary>
public partial class CommandInput
{
/// <summary>
/// Specified arguments.
/// </summary>
public IReadOnlyList<string> Arguments { get; }
/// <summary>
/// Specified directives.
/// </summary>
public IReadOnlyList<string> Directives { get; }
/// <summary>
/// Specified options.
/// </summary>
public IReadOnlyList<CommandOptionInput> Options { get; }
/// <summary>
/// Environment variables available when the command was parsed
/// </summary>
public IReadOnlyDictionary<string, string> EnvironmentVariables { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
IReadOnlyDictionary<string, string> environmentVariables)
{
Arguments = arguments;
Directives = directives;
Options = options;
EnvironmentVariables = environmentVariables;
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(arguments, directives, options, EmptyEnvironmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(arguments, EmptyDirectives, options, environmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options)
: this(arguments, EmptyDirectives, options)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<CommandOptionInput> options)
: this(new string[0], options)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<string> arguments)
: this(arguments, EmptyOptions)
{
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
foreach (var argument in Arguments)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(argument);
}
foreach (var directive in Directives)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(directive);
}
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(option);
}
return buffer.ToString();
}
}
public partial class CommandInput
{
private static readonly IReadOnlyList<string> EmptyDirectives = new string[0];
private static readonly IReadOnlyList<CommandOptionInput> EmptyOptions = new CommandOptionInput[0];
private static readonly IReadOnlyDictionary<string, string> EmptyEnvironmentVariables = new Dictionary<string, string>();
/// <summary>
/// Empty input.
/// </summary>
public static CommandInput Empty { get; } = new CommandInput(EmptyOptions);
}
}

View File

@@ -1,88 +0,0 @@
using System.Reflection;
using System.Text;
namespace CliFx.Models
{
/// <summary>
/// Schema of a defined command option.
/// </summary>
public partial class CommandOptionSchema
{
/// <summary>
/// Underlying property.
/// </summary>
public PropertyInfo? Property { get; }
/// <summary>
/// Option name.
/// </summary>
public string? Name { get; }
/// <summary>
/// Option short name.
/// </summary>
public char? ShortName { get; }
/// <summary>
/// Whether an option is required.
/// </summary>
public bool IsRequired { get; }
/// <summary>
/// Option description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified.
/// </summary>
public string? EnvironmentVariableName { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionSchema"/>.
/// </summary>
public CommandOptionSchema(PropertyInfo? property, string? name, char? shortName, bool isRequired, string? description, string? environmentVariableName)
{
Property = property;
Name = name;
ShortName = shortName;
IsRequired = isRequired;
Description = description;
EnvironmentVariableName = environmentVariableName;
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
if (IsRequired)
buffer.Append('*');
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
buffer.Append('|');
if (ShortName != null)
buffer.Append(ShortName);
return buffer.ToString();
}
}
public partial class CommandOptionSchema
{
// Here we define some built-in options.
// This is probably a bit hacky but I couldn't come up with a better solution given this architecture.
// We define them here to serve as a single source of truth, because they are used...
// ...in CliApplication (when reading) and HelpTextRenderer (when writing).
internal static CommandOptionSchema HelpOption { get; } =
new CommandOptionSchema(null, "help", 'h', false, "Shows help text.", null);
internal static CommandOptionSchema VersionOption { get; } =
new CommandOptionSchema(null, "version", null, false, "Shows version information.", null);
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using CliFx.Internal;
namespace CliFx.Models
{
/// <summary>
/// Schema of a defined command.
/// </summary>
public partial class CommandSchema
{
/// <summary>
/// Underlying type.
/// </summary>
public Type? Type { get; }
/// <summary>
/// Command name.
/// </summary>
public string? Name { get; }
/// <summary>
/// Command description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Command options.
/// </summary>
public IReadOnlyList<CommandOptionSchema> Options { get; }
/// <summary>
/// Command arguments.
/// </summary>
public IReadOnlyList<CommandArgumentSchema> Arguments { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandSchema"/>.
/// </summary>
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandArgumentSchema> arguments, IReadOnlyList<CommandOptionSchema> options)
{
Type = type;
Name = name;
Description = description;
Options = options;
Arguments = arguments;
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
if (Options != null)
{
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append('[');
buffer.Append(option);
buffer.Append(']');
}
}
return buffer.ToString();
}
}
public partial class CommandSchema
{
internal static CommandSchema StubDefaultCommand { get; } =
new CommandSchema(null, null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0]);
}
}

View File

@@ -1,131 +0,0 @@
using CliFx.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace CliFx.Models
{
/// <summary>
/// Extensions for <see cref="Models"/>.
/// </summary>
public static class Extensions
{
/// <summary>
/// Finds a command that has specified name, or null if not found.
/// </summary>
public static CommandSchema? FindByName(this IReadOnlyList<CommandSchema> commandSchemas, string? commandName)
{
// If looking for default command, don't compare names directly
// ...because null and empty are both valid names for default command
if (string.IsNullOrWhiteSpace(commandName))
return commandSchemas.FirstOrDefault(c => c.IsDefault());
return commandSchemas.FirstOrDefault(c => string.Equals(c.Name, commandName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Finds parent command to the command that has specified name, or null if not found.
/// </summary>
public static CommandSchema? FindParent(this IReadOnlyList<CommandSchema> commandSchemas, string? commandName)
{
// If command has no name, it's the default command so it doesn't have a parent
if (string.IsNullOrWhiteSpace(commandName))
return null;
// Repeatedly cut off individual words from the name until we find a command with that name
var temp = commandName;
while (temp.Contains(" "))
{
temp = temp.SubstringUntilLast(" ");
var parent = commandSchemas.FindByName(temp);
if (parent != null)
return parent;
}
// If no parent is matched by name, then the parent is the default command
return commandSchemas.FirstOrDefault(c => c.IsDefault());
}
/// <summary>
/// Determines whether an option schema matches specified alias.
/// </summary>
public static bool MatchesAlias(this CommandOptionSchema optionSchema, string alias)
{
// Compare against name. Case is ignored.
var matchesByName =
!string.IsNullOrWhiteSpace(optionSchema.Name) &&
string.Equals(optionSchema.Name, alias, StringComparison.OrdinalIgnoreCase);
// Compare against short name. Case is NOT ignored.
var matchesByShortName =
optionSchema.ShortName != null &&
alias.Length == 1 && alias[0] == optionSchema.ShortName;
return matchesByName || matchesByShortName;
}
/// <summary>
/// Finds an option input that matches the option schema specified, or null if not found.
/// </summary>
public static CommandOptionInput? FindByOptionSchema(this IReadOnlyList<CommandOptionInput> optionInputs, CommandOptionSchema optionSchema) =>
optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias));
/// <summary>
/// Gets valid aliases for the option.
/// </summary>
public static IReadOnlyList<string> GetAliases(this CommandOptionSchema optionSchema)
{
var result = new List<string>(2);
if (!string.IsNullOrWhiteSpace(optionSchema.Name))
result.Add(optionSchema.Name!);
if (optionSchema.ShortName != null)
result.Add(optionSchema.ShortName.Value.AsString());
return result;
}
/// <summary>
/// Gets whether a command was specified in the input.
/// </summary>
public static bool HasArguments(this CommandInput commandInput) => commandInput.Arguments.Any();
/// <summary>
/// Gets whether debug directive was specified in the input.
/// </summary>
public static bool IsDebugDirectiveSpecified(this CommandInput commandInput) =>
commandInput.Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets whether preview directive was specified in the input.
/// </summary>
public static bool IsPreviewDirectiveSpecified(this CommandInput commandInput) =>
commandInput.Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets whether help option was specified in the input.
/// </summary>
public static bool IsHelpOptionSpecified(this CommandInput commandInput)
{
var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias);
}
/// <summary>
/// Gets whether version option was specified in the input.
/// </summary>
public static bool IsVersionOptionSpecified(this CommandInput commandInput)
{
var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
}
/// <summary>
/// Gets whether this command is the default command, i.e. without a name.
/// </summary>
public static bool IsDefault(this CommandSchema commandSchema) => string.IsNullOrWhiteSpace(commandSchema.Name);
}
}

View File

@@ -1,37 +0,0 @@
using System.Collections.Generic;
namespace CliFx.Models
{
/// <summary>
/// Source information used to generate help text.
/// </summary>
public class HelpTextSource
{
/// <summary>
/// Application metadata.
/// </summary>
public ApplicationMetadata ApplicationMetadata { get; }
/// <summary>
/// Schemas of commands available in the application.
/// </summary>
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
/// <summary>
/// Schema of the command for which help text is to be generated.
/// </summary>
public CommandSchema TargetCommandSchema { get; }
/// <summary>
/// Initializes an instance of <see cref="HelpTextSource"/>.
/// </summary>
public HelpTextSource(ApplicationMetadata applicationMetadata,
IReadOnlyList<CommandSchema> availableCommandSchemas,
CommandSchema targetCommandSchema)
{
ApplicationMetadata = applicationMetadata;
AvailableCommandSchemas = availableCommandSchemas;
TargetCommandSchema = targetCommandSchema;
}
}
}

View File

@@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
/// <inheritdoc />
public class CommandArgumentSchemasValidator : ICommandArgumentSchemasValidator
{
private bool IsEnumerableArgument(CommandArgumentSchema schema)
{
return schema.Property.PropertyType != typeof(string) && schema.Property.PropertyType.GetEnumerableUnderlyingType() != null;
}
/// <inheritdoc />
public IEnumerable<ValidationError> ValidateArgumentSchemas(IReadOnlyCollection<CommandArgumentSchema> commandArgumentSchemas)
{
if (commandArgumentSchemas.Count == 0)
{
// No validation needed
yield break;
}
// Make sure there are no arguments with the same name
var duplicateNameGroups = commandArgumentSchemas
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.GroupBy(x => x.Name)
.Where(x => x.Count() > 1);
foreach (var schema in duplicateNameGroups)
{
yield return new ValidationError($"Multiple arguments with same name: \"{schema.Key}\".");
}
// Make sure that the order of all properties are distinct
var duplicateOrderGroups = commandArgumentSchemas
.GroupBy(x => x.Order)
.Where(x => x.Count() > 1);
foreach (var schema in duplicateOrderGroups)
{
yield return new ValidationError($"Multiple arguments with the same order: \"{schema.Key}\".");
}
var enumerableArguments = commandArgumentSchemas
.Where(IsEnumerableArgument)
.ToList();
// Verify that no more than one enumerable argument exists
if (enumerableArguments.Count > 1)
{
yield return new ValidationError($"Multiple sequence arguments found; only one is supported.");
}
// If an enumerable argument exists, ensure that it has the highest order
if (enumerableArguments.Count == 1)
{
if (enumerableArguments.Single().Order != commandArgumentSchemas.Max(x => x.Order))
{
yield return new ValidationError($"A sequence argument was defined with a lower order than another argument; the sequence argument must have the highest order (appear last).");
}
}
// Verify that all required arguments appear before optional arguments
if (commandArgumentSchemas.Any(x => x.IsRequired) && commandArgumentSchemas.Any(x => !x.IsRequired) &&
commandArgumentSchemas.Where(x => x.IsRequired).Max(x => x.Order) > commandArgumentSchemas.Where(x => !x.IsRequired).Min(x => x.Order))
{
yield return new ValidationError("One or more required arguments appear after optional arguments. Required arguments must appear before (i.e. have lower order than) optional arguments.");
}
}
}
/// <summary>
/// Represents a failed validation.
/// </summary>
public class ValidationError
{
/// <summary>
/// Creates an instance of <see cref="ValidationError"/> with a message.
/// </summary>
public ValidationError(string message)
{
Message = message;
}
/// <summary>
/// The error message for the failed validation.
/// </summary>
public string Message { get; }
}
}

View File

@@ -1,14 +0,0 @@
using System;
using CliFx.Models;
namespace CliFx.Services
{
/// <summary>
/// Default implementation of <see cref="ICommandFactory"/>.
/// </summary>
public class CommandFactory : ICommandFactory
{
/// <inheritdoc />
public ICommand CreateCommand(CommandSchema commandSchema) => (ICommand) Activator.CreateInstance(commandSchema.Type);
}
}

Some files were not shown because too many files have changed in this diff Show More