mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Rework (#36)
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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`.
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
48
CliFx.Tests/DefaultCommandFactoryTests.cs
Normal file
48
CliFx.Tests/DefaultCommandFactoryTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CliFx.Tests/DelegateCommandFactoryTests.cs
Normal file
33
CliFx.Tests/DelegateCommandFactoryTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
888
CliFx.Tests/Domain/ApplicationSchemaTests.cs
Normal file
888
CliFx.Tests/Domain/ApplicationSchemaTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
264
CliFx.Tests/Domain/CommandLineInputTests.cs
Normal file
264
CliFx.Tests/Domain/CommandLineInputTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
126
CliFx.Tests/TestCommands/AllSupportedTypesCommand.cs
Normal file
126
CliFx.Tests/TestCommands/AllSupportedTypesCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
CliFx.Tests/TestCommands/BrokenEnumerableCommand.cs
Normal file
15
CliFx.Tests/TestCommands/BrokenEnumerableCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 +9,7 @@ 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
17
CliFx.Tests/TestCommands/DuplicateParameterNameCommand.cs
Normal file
17
CliFx.Tests/TestCommands/DuplicateParameterNameCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
CliFx.Tests/TestCommands/DuplicateParameterOrderCommand.cs
Normal file
17
CliFx.Tests/TestCommands/DuplicateParameterOrderCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
namespace CliFx.Tests.TestCommands
|
||||||
{
|
{
|
||||||
|
|||||||
18
CliFx.Tests/TestCommands/NonLastNonScalarParameterCommand.cs
Normal file
18
CliFx.Tests/TestCommands/NonLastNonScalarParameterCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
CliFx.Tests/TestCommands/ParameterCommand.cs
Normal file
24
CliFx.Tests/TestCommands/ParameterCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
CliFx.Tests/TestCommands/SimpleParameterCommand.cs
Normal file
20
CliFx.Tests/TestCommands/SimpleParameterCommand.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
14
CliFx.Tests/TestCustomTypes/TestCustomEnumerable.cs
Normal file
14
CliFx.Tests/TestCustomTypes/TestCustomEnumerable.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace CliFx.Models
|
namespace CliFx
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Metadata associated with an application.
|
/// Metadata associated with an application.
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
37
CliFx/Attributes/CommandParameterAttribute.cs
Normal file
37
CliFx/Attributes/CommandParameterAttribute.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
314
CliFx/CliApplication.Help.cs
Normal file
314
CliFx/CliApplication.Help.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
|
||||||
|
|
||||||
|
return ex is CommandException commandException
|
||||||
|
? commandException.ExitCode
|
||||||
|
: ex.HResult;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return exit code if it was specified via CommandException
|
/// <summary>
|
||||||
if (ex is CommandException commandException)
|
/// 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)
|
||||||
{
|
{
|
||||||
return commandException.ExitCode;
|
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||||
|
.Cast<DictionaryEntry>()
|
||||||
|
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return await RunAsync(commandLineArguments, environmentVariables);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the application and returns the exit code.
|
||||||
|
/// Command line arguments and environment variables are retrieved automatically.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask<int> RunAsync()
|
||||||
{
|
{
|
||||||
return ex.HResult;
|
var commandLineArguments = Environment.GetCommandLineArgs()
|
||||||
}
|
.Skip(1)
|
||||||
}
|
.ToArray();
|
||||||
|
|
||||||
|
return await RunAsync(commandLineArguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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'">
|
||||||
|
|||||||
29
CliFx/DefaultTypeActivator.cs
Normal file
29
CliFx/DefaultTypeActivator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
CliFx/DelegateTypeActivator.cs
Normal file
20
CliFx/DelegateTypeActivator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
256
CliFx/Domain/ApplicationSchema.cs
Normal file
256
CliFx/Domain/ApplicationSchema.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
CliFx/Domain/CommandArgumentSchema.cs
Normal file
176
CliFx/Domain/CommandArgumentSchema.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
CliFx/Domain/CommandLineInput.cs
Normal file
179
CliFx/Domain/CommandLineInput.cs
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
105
CliFx/Domain/CommandOptionSchema.cs
Normal file
105
CliFx/Domain/CommandOptionSchema.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
53
CliFx/Domain/CommandParameterSchema.cs
Normal file
53
CliFx/Domain/CommandParameterSchema.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
CliFx/Domain/CommandSchema.cs
Normal file
196
CliFx/Domain/CommandSchema.cs
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
15
CliFx/ITypeActivator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
CliFx/Internal/Polyfills.cs
Normal file
16
CliFx/Internal/Polyfills.cs
Normal 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
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user