22 Commits
0.0.8 ... 1.0

Author SHA1 Message Date
Alexey Golub
4365ad457a Update version 2020-01-30 12:31:37 +02:00
Alexey Golub
fb3617980e Add argument syntax info to readme 2020-01-30 12:28:56 +02:00
Alexey Golub
7690aae456 Update readme 2020-01-30 11:48:31 +02:00
Alexey Golub
076678a08c Change how headers are rendered in help text 2020-01-30 11:44:36 +02:00
Alexey Golub
104279d6e9 Change how non-scalar arguments are displayed in usage 2020-01-30 10:58:36 +02:00
Alexey Golub
515d51a91d Add dummy tests to cover difficult scenarios 2020-01-29 23:35:45 +02:00
Alexey Golub
4fdf543190 Ensure delegate type activator doesn't return null 2020-01-29 20:03:06 +02:00
Alexey Golub
4e1ab096c9 Add info about environment variables 2020-01-29 19:40:58 +02:00
Alexey Golub
8aa6911cca Update readme 2020-01-29 10:59:45 +02:00
Alexey Golub
f0362019ed Use lowercase default display name for parameters 2020-01-28 22:24:38 +02:00
Alexey Golub
82895f2e42 Improve preview directive 2020-01-28 14:48:21 +02:00
Alexey Golub
4cf622abe5 Add Cocona to benchmarks 2020-01-27 21:50:25 +02:00
Alexey Golub
d4e22a78d6 Change to a more permissive license 2020-01-27 21:35:14 +02:00
Alexey Golub
3883c831e9 Rework (#36) 2020-01-27 21:10:14 +02:00
dgarcia202
63441688fe Add required options to the usage help text (#35) 2020-01-17 23:05:01 +02:00
Thorkil Holm-Jacobsen
e48839b938 Add positional arguments (#32) 2020-01-13 13:31:05 +02:00
Alexey Golub
ed87373dc3 Make exceptions slightly more friendly 2019-12-16 22:58:31 +02:00
Alexey Golub
6ce52c70f7 Use ValueTask 2019-12-16 22:16:16 +02:00
Alexey Golub
d2b0b16121 Use shared props files 2019-12-01 16:08:41 +02:00
Alexey Golub
d67a9fe762 Update readme 2019-11-30 23:32:06 +02:00
Alexey Golub
ce2a3153e6 Update donation info 2019-11-19 21:14:15 +02:00
Alexey Golub
d4b54231fb Remove unnecessary CD step 2019-11-13 20:41:57 +02:00
126 changed files with 4275 additions and 4344 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,3 @@
github: Tyrrrz github: Tyrrrz
patreon: Tyrrrz patreon: Tyrrrz
open_collective: Tyrrrz custom: ['buymeacoffee.com/Tyrrrz', 'tyrrrz.me/donate']
custom: ['buymeacoffee.com/Tyrrrz']

View File

@@ -16,12 +16,10 @@ jobs:
- name: Install .NET Core - name: Install .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 3.0.100 dotnet-version: 3.1.100
- name: Pack - name: Pack
run: dotnet pack CliFx --configuration Release run: dotnet pack CliFx --configuration Release
- name: Deploy - name: Deploy
run: | run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}
dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}
dotnet nuget push CliFx/bin/Release/*.snupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install .NET Core - name: Install .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 3.0.100 dotnet-version: 3.1.100
- name: Build & test - name: Build & test
run: dotnet test --configuration Release run: dotnet test --configuration Release

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,35 +1,46 @@
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
{ {
[CoreJob] [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 Task<int> ExecuteWithCliFx() => 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 Task<int> ExecuteWithSystemCommandLine() => 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 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();
[Benchmark(Description = "Cocona")]
public void ExecuteWithCocona() =>
Cocona.CoconaApp.Run<CoconaCommand>(Arguments);
} }
} }

View File

@@ -1,16 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> <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="Cocona" Version="1.0.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" /> <PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.5.0" />
<PackageReference Include="PowerArgs" Version="3.6.0" /> <PackageReference Include="PowerArgs" Version="3.6.0" />
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" /> <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Benchmarks.Commands namespace CliFx.Benchmarks.Commands
{ {
@@ -16,6 +15,6 @@ namespace CliFx.Benchmarks.Commands
[CommandOption("bool", 'b')] [CommandOption("bool", 'b')]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -0,0 +1,17 @@
using Cocona;
namespace CliFx.Benchmarks.Commands
{
public class CoconaCommand
{
public void Execute(
[Option("str", new []{'s'})]
string? strOption,
[Option("int", new []{'i'})]
int intOption,
[Option("bool", new []{'b'})]
bool boolOption)
{
}
}
}

View File

@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,7 +5,6 @@ using CliFx.Demo.Internal;
using CliFx.Demo.Models; using CliFx.Demo.Models;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -14,31 +13,25 @@ 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; } = "";
[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)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task 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);
@@ -48,7 +41,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine("Book added."); console.Output.WriteLine("Book added.");
console.RenderBook(book); console.RenderBook(book);
return Task.CompletedTask; return default;
} }
} }
@@ -65,7 +58,7 @@ namespace CliFx.Demo.Commands
Random.Next(1, 59), Random.Next(1, 59),
TimeSpan.Zero); TimeSpan.Zero);
public static Isbn CreateRandomIsbn() => new Isbn( private static Isbn CreateRandomIsbn() => new Isbn(
Random.Next(0, 999), Random.Next(0, 999),
Random.Next(0, 99), Random.Next(0, 99),
Random.Next(0, 99999), Random.Next(0, 99999),

View File

@@ -3,7 +3,6 @@ using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -12,15 +11,15 @@ 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)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);
@@ -29,7 +28,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book); console.RenderBook(book);
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -16,7 +15,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var library = _libraryService.GetLibrary(); var library = _libraryService.GetLibrary();
@@ -32,7 +31,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book); console.RenderBook(book);
} }
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -11,15 +10,15 @@ 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)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);
@@ -30,7 +29,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine($"Book {Title} removed."); console.Output.WriteLine($"Book {Title} removed.");
return Task.CompletedTask; return default;
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Dummy.Commands
{
[Command]
public class HelloWorldCommand : ICommand
{
[CommandOption("target", EnvironmentVariableName = "ENV_TARGET")]
public string Target { get; set; } = "World";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine($"Hello {Target}!");
return default;
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace CliFx.Tests.Dummy
{
public class Program
{
public static async Task Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build()
.RunAsync();
}
}

View File

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

View File

@@ -5,8 +5,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
namespace CliFx.Tests namespace CliFx.Tests
@@ -14,6 +12,7 @@ namespace CliFx.Tests
[TestFixture] [TestFixture]
public class CliApplicationTests public class CliApplicationTests
{ {
private const string TestAppName = "TestApp";
private const string TestVersionText = "v1.0"; private const string TestVersionText = "v1.0";
private static IEnumerable<TestCaseData> GetTestCases_RunAsync() private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
@@ -21,102 +20,105 @@ namespace CliFx.Tests
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new string[0], new string[0],
new Dictionary<string, string>(),
"Hello world." "Hello world."
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "}, new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
new Dictionary<string, string>(),
"foo bar" "foo bar"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "one", "two", "three", "-s", ", "}, new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
new Dictionary<string, string>(),
"one, two, three" "one, two, three"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DivideCommand)}, new[] {typeof(DivideCommand)},
new[] {"div", "-D", "24", "-d", "8"}, new[] {"div", "-D", "24", "-d", "8"},
new Dictionary<string, string>(),
"3" "3"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--version"}, new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"--version"}, new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText TestVersionText
); );
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"-h"},
null
);
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--help"},
null
);
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new string[0], new string[0],
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"-h"}, new[] {"-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"--help"}, new[] {"--help"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "-h"}, new[] {"concat", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] {typeof(ExceptionCommand)},
new[] {"exc", "-h"}, new[] {"exc", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-h"}, new[] {"exc", "-h"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"[preview]"}, new[] {"[preview]"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] {typeof(ExceptionCommand)},
new[] {"exc", "[preview]"}, new[] {"[preview]", "exc"},
new Dictionary<string, string>(),
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] {typeof(ConcatCommand)},
new[] {"concat", "[preview]", "-o", "value"}, new[] {"[preview]", "concat", "-o", "value"},
new Dictionary<string, string>(),
null null
); );
} }
@@ -126,109 +128,292 @@ 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]"
}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
new[] {"--help"},
new[]
{
"Environment variable:", "ENV_SINGLE_VALUE"
}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "--help"},
new[]
{
"Usage",
TestAppName, "concat", "-i", "<values...>", "[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 +421,31 @@ namespace CliFx.Tests
{ {
// Arrange // Arrange
using var cancellationTokenSource = new CancellationTokenSource(); using var cancellationTokenSource = new CancellationTokenSource();
await using var stdoutStream = new StringWriter();
var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token); await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand)) .AddCommand(typeof(CancellableCommand))
.UseConsole(console) .UseConsole(console)
.Build(); .Build();
var args = new[] {"cancel"};
var commandLineArguments = new[] {"cancel"};
var environmentVariables = new Dictionary<string, string>();
// Act // Act
var runTask = application.RunAsync(args); cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2));
cancellationTokenSource.Cancel(); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var exitCode = await runTask.ConfigureAwait(false); var stdOut = stdOutStream.ToString().Trim();
var stdOut = stdoutStream.ToString().Trim(); var stdErr = stdErrStream.ToString().Trim();
// Assert // Assert
exitCode.Should().Be(-2146233029); exitCode.Should().NotBe(0);
stdOut.Should().Be("Printed"); stdOut.Should().BeNullOrWhiteSpace();
stdErr.Should().NotBeNullOrWhiteSpace();
Console.WriteLine(stdErr);
} }
} }
} }

View File

@@ -1,25 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat> <CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput> <CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.9.0" /> <PackageReference Include="CliWrap" Version="2.5.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>
<ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" />
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="Copy dummy's runtime config" AfterTargets="AfterBuild">
<Copy SourceFiles="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json" DestinationFiles="$(OutputPath)CliFx.Tests.Dummy.runtimeconfig.json" />
</Target>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

82
CliFx.Tests/DummyTests.cs Normal file
View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CliWrap;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DummyTests
{
private static Assembly DummyAssembly { get; } = typeof(Dummy.Program).Assembly;
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
yield return new TestCaseData(
new[] {"--version"},
new Dictionary<string, string>(),
$"v{DummyAssembly.GetName().Version}"
);
yield return new TestCaseData(
new string[0],
new Dictionary<string, string>(),
"Hello World!"
);
yield return new TestCaseData(
new[] {"--target", "Earth"},
new Dictionary<string, string>(),
"Hello Earth!"
);
yield return new TestCaseData(
new string[0],
new Dictionary<string, string>
{
["ENV_TARGET"] = "Mars"
},
"Hello Mars!"
);
yield return new TestCaseData(
new[] {"--target", "Earth"},
new Dictionary<string, string>
{
["ENV_TARGET"] = "Mars"
},
"Hello Earth!"
);
}
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(
IReadOnlyList<string> arguments,
IReadOnlyDictionary<string, string> environmentVariables,
string expectedStdOut)
{
// Arrange
var cli = Cli.Wrap("dotnet")
.SetArguments(arguments.Prepend(DummyAssembly.Location).ToArray())
.EnableExitCodeValidation()
.EnableStandardErrorValidation()
.SetStandardOutputCallback(Console.WriteLine)
.SetStandardErrorCallback(Console.WriteLine);
foreach (var (key, value) in environmentVariables)
cli.SetEnvironmentVariable(key, value);
// Act
var result = await cli.ExecuteAsync();
// Assert
result.ExitCode.Should().Be(0);
result.StandardError.Should().BeNullOrWhiteSpace();
result.StandardOutput.TrimEnd().Should().Be(expectedStdOut);
}
}
}

View File

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

View File

@@ -1,174 +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().GetCommandSchemas(new[] { commandType }).Single();
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
{
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8")
}),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8")
}),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8")
}),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat", new[]
{
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
}),
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("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(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, 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(),
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
new CommandInput(null, 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(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, 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(),
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
{
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div")
);
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("D", "13")
})
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat")
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat", new[]
{
new CommandOptionInput("s", "_")
})
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput,
ICommand expectedCommand)
{
// Arrange
var initializer = new CommandInitializer();
// Act
initializer.InitializeCommand(command, commandSchema, commandInput);
// Assert
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
{
// Arrange
var initializer = new CommandInitializer();
// Act & Assert
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
.Should().ThrowExactly<CliFxException>();
}
}
}

View File

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

View File

@@ -1,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 CommandOptionInputConverterTests
{
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 CommandOptionInputConverter();
// 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 CommandOptionInputConverter();
// Act & Assert
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
.Should().ThrowExactly<CliFxException>();
}
}
}

View File

@@ -1,116 +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[]
{
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[]
{
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[]
{
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 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)}
});
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
IReadOnlyList<CommandSchema> expectedCommandSchemas)
{
// Arrange
var commandSchemaResolver = new CommandSchemaResolver();
// 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();
// Act & Assert
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
.Should().ThrowExactly<CliFxException>();
}
}
}

View File

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

View File

@@ -1,108 +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();
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."
}
);
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."
}
);
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."
}
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_RenderHelpText))]
public void RenderHelpText_Test(HelpTextSource source,
IReadOnlyList<string> expectedSubstrings)
{
// 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);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("allrequired", Description = "AllRequiredOptionsCommand description.")]
public class AllRequiredOptionsCommand : ICommand
{
[CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")]
public string? OptionF { get; set; }
[CommandOption("option-g", 'g', IsRequired = true, Description = "OptionG description.")]
public string? OptionFG { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

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

View File

@@ -1,22 +1,16 @@
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
{ {
[Command("cancel")] [Command("cancel")]
public class CancellableCommand : ICommand public class CancellableCommand : ICommand
{ {
public async Task ExecuteAsync(IConsole console) public async ValueTask ExecuteAsync(IConsole console)
{ {
await Task.Yield(); await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), console.GetCancellationToken()).ConfigureAwait(false);
console.Output.WriteLine("Never printed"); console.Output.WriteLine("Never printed");
} }
} }
} }

View File

@@ -1,7 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -14,6 +13,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("msg", 'm')] [CommandOption("msg", 'm')]
public string? Message { get; set; } public string? Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode); public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
} }
} }

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -14,10 +13,10 @@ namespace CliFx.Tests.TestCommands
[CommandOption('s', Description = "String separator.")] [CommandOption('s', Description = "String separator.")]
public string Separator { get; set; } = ""; public string Separator { get; set; } = "";
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine(string.Join(Separator, Inputs)); console.Output.WriteLine(string.Join(Separator, Inputs));
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -16,10 +15,10 @@ namespace CliFx.Tests.TestCommands
// This property should be ignored by resolver // This property should be ignored by resolver
public bool NotAnOption { get; set; } public bool NotAnOption { get; set; }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine(Dividend / Divisor); console.Output.WriteLine(Dividend / Divisor);
return Task.CompletedTask; return default;
} }
} }
} }

View File

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

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -13,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("fruits")] [CommandOption("fruits")]
public string? Oranges { get; set; } public string? Oranges { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -10,6 +9,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] [CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string? Option { get; set; } public string? Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -11,6 +10,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public IEnumerable<string>? Option { get; set; } public IEnumerable<string>? Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -10,6 +9,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public string? Option { get; set; } public string? Option { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -11,6 +10,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("msg", 'm')] [CommandOption("msg", 'm')]
public string? Message { get; set; } public string? Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new Exception(Message); public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
} }
} }

View File

@@ -1,16 +1,15 @@
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 HelloWorldDefaultCommand : ICommand public class HelloWorldDefaultCommand : ICommand
{ {
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
console.Output.WriteLine("Hello world."); console.Output.WriteLine("Hello world.");
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -13,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-b", 'b', Description = "OptionB description.")] [CommandOption("option-b", 'b', Description = "OptionB description.")]
public string? OptionB { get; set; } public string? OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -13,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-d", 'd', Description = "OptionD description.")] [CommandOption("option-d", 'd', Description = "OptionD description.")]
public string? OptionD { get; set; } public string? OptionD { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
@@ -10,6 +9,6 @@ namespace CliFx.Tests.TestCommands
[CommandOption("option-e", 'e', Description = "OptionE description.")] [CommandOption("option-e", 'e', Description = "OptionE description.")]
public string? OptionE { get; set; } public string? OptionE { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

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

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services;
namespace CliFx.Tests.TestCommands namespace CliFx.Tests.TestCommands
{ {
public class NonAnnotatedCommand : ICommand public class NonAnnotatedCommand : ICommand
{ {
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("somerequired", Description = "SomeRequiredOptionsCommand description.")]
public class SomeRequiredOptionsCommand : ICommand
{
[CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")]
public string? OptionF { get; set; }
[CommandOption("option-g", 'g', Description = "OptionG description.")]
public string? OptionFG { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

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

View File

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

11
CliFx.props Normal file
View File

@@ -0,0 +1,11 @@
<Project>
<PropertyGroup>
<Version>1.0</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -12,12 +12,15 @@ 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}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -76,6 +79,18 @@ Global
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,324 @@
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)
{
RenderIndent(spaces - column);
}
else
{
Render(" ");
}
}
void RenderWithColor(string text, ConsoleColor foregroundColor)
{
_console.WithForegroundColor(foregroundColor, () => Render(text));
}
void RenderHeader(string text)
{
RenderWithColor(text, ConsoleColor.Magenta);
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.IsScalar
? $"<{parameter.DisplayName}>"
: $"<{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(option.IsScalar
? "<value>"
: "<values...>");
}
else
{
RenderWithColor($"-{option.ShortName}", ConsoleColor.White);
Render(" ");
Render(option.IsScalar
? "<value>"
: "<values...>");
}
}
// 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);
RenderColumnIndent();
// Description
if (!string.IsNullOrWhiteSpace(parameter.Description))
{
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);
}
RenderColumnIndent();
// Description
if (!string.IsNullOrWhiteSpace(option.Description))
{
Render(option.Description);
Render(" ");
}
// Environment variable
if (!string.IsNullOrWhiteSpace(option.EnvironmentVariableName))
{
Render($"(Environment variable: {option.EnvironmentVariableName}).");
Render(" ");
}
RenderNewLine();
}
}
void RenderChildCommands()
{
if (!childCommands.Any())
return;
RenderMargin();
RenderHeader("Commands");
foreach (var childCommand in childCommands)
{
var relativeCommandName =
string.IsNullOrWhiteSpace(childCommand.Name) || string.IsNullOrWhiteSpace(command.Name)
? childCommand.Name
: childCommand.Name.Substring(command.Name.Length + 1);
// Name
RenderIndent();
RenderWithColor(relativeCommandName, ConsoleColor.Cyan);
// Description
if (!string.IsNullOrWhiteSpace(childCommand.Description))
{
RenderColumnIndent();
Render(childCommand.Description);
}
RenderNewLine();
}
RenderMargin();
// Child command help tip
Render("You can run `");
Render(_metadata.ExecutableName);
if (!string.IsNullOrWhiteSpace(command.Name))
{
Render(" ");
RenderWithColor(command.Name, ConsoleColor.Cyan);
}
Render(" ");
RenderWithColor("[command]", ConsoleColor.Cyan);
Render(" ");
RenderWithColor("--help", ConsoleColor.White);
Render("` to show help on a specific command.");
RenderNewLine();
}
_console.ResetColor();
RenderApplicationInfo();
RenderDescription();
RenderUsage();
RenderParameters();
RenderOptions();
RenderChildCommands();
}
}
}

View File

@@ -1,203 +1,162 @@
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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal;
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 Task<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(ApplicationSchema applicationSchema, 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 var commandSchema = applicationSchema.TryFindCommand(commandLineInput, out var argumentOffset);
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
_console.Output.WriteLine();
// Render directives _console.Output.WriteLine("Parser preview:");
_console.Output.WriteLine("Directives:");
foreach (var directive in commandInput.Directives) // Command name
if (commandSchema != null && argumentOffset > 0)
{ {
_console.Output.Write(" "); _console.WithForegroundColor(ConsoleColor.Cyan, () =>
_console.Output.WriteLine(directive); _console.Output.Write(commandSchema.Name));
_console.Output.Write(' ');
} }
// Margin // Parameters
_console.Output.WriteLine(); foreach (var parameter in commandLineInput.Arguments.Skip(argumentOffset))
// Render options
_console.Output.WriteLine("Options:");
foreach (var option in commandInput.Options)
{ {
_console.Output.Write(" "); _console.Output.Write('<');
_console.Output.WriteLine(option);
_console.WithForegroundColor(ConsoleColor.White, () =>
_console.Output.Write(parameter));
_console.Output.Write('>');
_console.Output.Write(' ');
} }
// Short-circuit with exit code 0 // Options
foreach (var option in commandLineInput.Options)
{
_console.Output.Write('[');
_console.WithForegroundColor(ConsoleColor.White, () =>
_console.Output.Write(option));
_console.Output.Write(']');
_console.Output.Write(' ');
}
_console.Output.WriteLine();
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.IsCommandSpecified() && 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(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema? targetCommandSchema)
{ {
// 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() || targetCommandSchema == 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;
// If target command isn't defined, find its contextual replacement RenderHelp(applicationSchema, commandSchema);
if (targetCommandSchema == null)
{
// If command was specified, inform the user that it's not defined
if (commandInput.IsCommandSpecified())
{
_console.WithForegroundColor(ConsoleColor.Red,
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
isError = true;
}
// Replace target command with closest parent of specified command
targetCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
// If there's no parent, replace with stub default command
if (targetCommandSchema == null)
{
targetCommandSchema = CommandSchema.StubDefaultCommand;
availableCommandSchemas = availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray();
}
}
// Build help text source
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
// Render help text
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
// Short-circuit with appropriate exit code
return isError ? -1 : 0;
}
private async Task<int> HandleCommandExecutionAsync(CommandInput commandInput, CommandSchema targetCommandSchema)
{
// Create an instance of the command
var command = _commandFactory.CreateCommand(targetCommandSchema);
// Populate command with options according to its schema
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// 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 Task<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 targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
// Chain handlers until the first one that produces an exit code
return return
await HandleDebugDirectiveAsync(commandInput) ?? await HandleDebugDirectiveAsync(commandLineInput) ??
HandlePreviewDirective(commandInput) ?? HandlePreviewDirective(applicationSchema, commandLineInput) ??
HandleVersionOption(commandInput) ?? HandleVersionOption(commandLineInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ?? HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(commandInput, targetCommandSchema!); await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -205,25 +164,42 @@ namespace CliFx
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions. // Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
// Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException // Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
if (!string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)) var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
{ ? ex.Message
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message)); : ex.ToString();
}
else
{
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex));
}
// Return exit code if it was specified via CommandException _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
if (ex is CommandException commandException)
{ return ex is CommandException commandException
return commandException.ExitCode; ? commandException.ExitCode
} : ex.HResult;
else
{
return ex.HResult;
}
} }
} }
/// <summary>
/// Runs the application with specified command line arguments and returns the exit code.
/// Environment variables are retrieved automatically.
/// </summary>
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{
var environmentVariables = Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.OrdinalIgnoreCase);
return await RunAsync(commandLineArguments, environmentVariables);
}
/// <summary>
/// Runs the application and returns the exit code.
/// Command line arguments and environment variables are retrieved automatically.
/// </summary>
public async ValueTask<int> RunAsync()
{
var commandLineArguments = Environment.GetCommandLineArgs()
.Skip(1)
.ToArray();
return await RunAsync(commandLineArguments);
}
} }
} }

View File

@@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using CliFx.Attributes; using CliFx.Domain;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplicationBuilder"/>. /// Builds an instance of <see cref="CliApplication"/>.
/// </summary> /// </summary>
public partial class CliApplicationBuilder : ICliApplicationBuilder public partial class CliApplicationBuilder
{ {
private readonly HashSet<Type> _commandTypes = new HashSet<Type>(); private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
@@ -24,121 +21,153 @@ namespace CliFx
private string? _versionText; private string? _versionText;
private string? _description; private string? _description;
private IConsole? _console; private IConsole? _console;
private ICommandFactory? _commandFactory; private ITypeActivator? _typeActivator;
private ICommandOptionInputConverter? _commandOptionInputConverter;
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(ICommandOptionInputConverter converter) /// Configures the application to use the specified function for activating types.
{ /// </summary>
_commandOptionInputConverter = 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();
_commandOptionInputConverter ??= new CommandOptionInputConverter();
_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(),
_commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
} }
} }
@@ -149,9 +178,9 @@ namespace CliFx
// Entry assembly is null in tests // Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value; private static Assembly EntryAssembly => LazyEntryAssembly.Value;
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name ?? ""; private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
private static string GetDefaultExecutableName() private static string? GetDefaultExecutableName()
{ {
var entryAssemblyLocation = EntryAssembly?.Location; var entryAssemblyLocation = EntryAssembly?.Location;
@@ -165,6 +194,9 @@ namespace CliFx
return Path.GetFileNameWithoutExtension(entryAssemblyLocation); return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
} }
private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : ""; private static string? GetDefaultVersionText() =>
EntryAssembly != null
? $"v{EntryAssembly.GetName().Version}"
: null;
} }
} }

View File

@@ -1,36 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net45;netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.1;netstandard2.0;net45</TargetFrameworks>
<Version>0.0.8</Version>
<Company>Tyrrrz</Company>
<Authors>$(Company)</Authors> <Authors>$(Company)</Authors>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<Description>Declarative framework for CLI applications</Description> <Description>Declarative framework for CLI applications</Description>
<PackageTags>command line executable interface framework parser arguments net core</PackageTags> <PackageTags>command line executable interface framework parser arguments net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl> <PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
<PackageIcon>favicon.png</PackageIcon> <PackageIcon>favicon.png</PackageIcon>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<PublishRepositoryUrl>True</PublishRepositoryUrl> <PublishRepositoryUrl>True</PublishRepositoryUrl>
<EmbedUntrackedSources>True</EmbedUntrackedSources> <EmbedUntrackedSources>True</EmbedUntrackedSources>
<IncludeSymbols>True</IncludeSymbols> <IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="all" /> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="all" /> <_Parameter1>$(AssemblyName).Tests</_Parameter1>
<PackageReference Include="Nullable" Version="1.1.1" PrivateAssets="all" /> </AssemblyAttribute>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" /> <None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup> </ItemGroup>
</Project> <ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.2.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net45'">
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.3" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,27 @@
using System;
using System.Text;
using CliFx.Exceptions;
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) ?? throw new CliFxException(new StringBuilder()
.Append($"Failed to create an instance of type {type.FullName}, received <null> instead.").Append(" ")
.Append("Make sure that the provided type activator was configured correctly.").Append(" ")
.Append("If you are using a dependency container, make sure that this type is registered.")
.ToString());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

15
CliFx/ITypeActivator.cs Normal file
View File

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

View File

@@ -12,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);
@@ -49,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();
} }
@@ -63,8 +58,5 @@ namespace CliFx.Internal
return array; return array;
} }
public static bool IsCollection(this Type type) =>
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
} }
} }

View File

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

View File

@@ -1,120 +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 command name.
/// Can be null if command was not specified.
/// </summary>
public string? CommandName { 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(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
IReadOnlyDictionary<string, string> environmentVariables)
{
CommandName = commandName;
Directives = directives;
Options = options;
EnvironmentVariables = environmentVariables;
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(commandName, directives, options, EmptyEnvironmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(commandName, EmptyDirectives, options, environmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
: this(commandName, EmptyDirectives, options)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<CommandOptionInput> options)
: this(null, options)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName)
: this(commandName, EmptyOptions)
{
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(CommandName))
buffer.Append(CommandName);
foreach (var directive in Directives)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(directive);
}
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(option);
}
return buffer.ToString();
}
}
public partial class CommandInput
{
private static readonly IReadOnlyList<string> EmptyDirectives = new string[0];
private static readonly IReadOnlyList<CommandOptionInput> EmptyOptions = new CommandOptionInput[0];
private static readonly IReadOnlyDictionary<string, string> EmptyEnvironmentVariables = new Dictionary<string, string>();
/// <summary>
/// Empty input.
/// </summary>
public static CommandInput Empty { get; } = new CommandInput(EmptyOptions);
}
}

View File

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

View File

@@ -1,69 +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>
/// Initializes an instance of <see cref="CommandSchema"/>.
/// </summary>
public CommandSchema(Type? type, string? name, string? description, IReadOnlyList<CommandOptionSchema> options)
{
Type = type;
Name = name;
Description = description;
Options = options;
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
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 CommandOptionSchema[0]);
}
}

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