31 Commits
0.0.6 ... 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
Alexey Golub
70bfe0bf91 Update version 2019-11-13 20:34:11 +02:00
Alexey Golub
9690c380d3 Use C#8 features and cleanup 2019-11-13 20:31:48 +02:00
Alexey Golub
85caa275ae Add source link 2019-11-12 22:26:29 +02:00
Federico Paolillo
32026e59c0 Use Path.Separator in environment variables tests (#31) 2019-11-09 13:06:00 +02:00
Alexey Golub
486ccb9685 Update csproj 2019-11-08 13:21:53 +02:00
Alexey Golub
7b766f70f3 Use GitHub actions 2019-11-06 15:08:51 +02:00
Alexey Golub
f73e96488f Update version 2019-10-31 14:42:30 +02:00
Moophic
af63fa5a1f Refactor cancellation (#30) 2019-10-31 14:39:56 +02:00
Moophic
e8f53c9463 Updated readme with cancellation info (#29) 2019-10-30 19:49:43 +02:00
135 changed files with 4584 additions and 4744 deletions

3
.github/FUNDING.yml vendored
View File

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

25
.github/workflows/CD.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: CD
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.100
- name: Pack
run: dotnet pack CliFx --configuration Release
- name: Deploy
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}}

22
.github/workflows/CI.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.100
- name: Build & test
run: dotnet test --configuration Release
- name: Coverage
run: curl -s https://codecov.io/bash | bash -s -- -f CliFx.Tests/bin/Release/Coverage.xml -t ${{secrets.CODECOV_TOKEN}} -Z

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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using CliFx.Benchmarks.Commands;
using CommandLine;
namespace CliFx.Benchmarks
{
[CoreJob]
[SimpleJob]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Benchmark
{
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[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")]
public Task<int> ExecuteWithSystemCommandLine() => new SystemCommandLineCommand().ExecuteAsync(Arguments);
public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
[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")]
public void ExecuteWithCommandLineParser()
{
var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand));
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
}
public void ExecuteWithCommandLineParser() =>
new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(c => c.Execute());
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
public void ExecuteWithPowerArgs() =>
PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
[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,15 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
<PackageReference Include="Cocona" Version="1.0.0" />
<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="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
</ItemGroup>

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Benchmarks.Commands
{
@@ -9,7 +7,7 @@ namespace CliFx.Benchmarks.Commands
public class CliFxCommand : ICommand
{
[CommandOption("str", 's')]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[CommandOption("int", 'i')]
public int IntOption { get; set; }
@@ -17,6 +15,6 @@ namespace CliFx.Benchmarks.Commands
[CommandOption("bool", 'b')]
public bool BoolOption { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class CliprCommand
{
[NamedArgument('s', "str")]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[NamedArgument('i', "int")]
public int IntOption { get; set; }

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

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class CommandLineParserCommand
{
[Option('s', "str")]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[Option('i', "int")]
public int IntOption { get; set; }

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class McMasterCommand
{
[Option("--str|-s")]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[Option("--int|-i")]
public int IntOption { get; set; }

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class PowerArgsCommand
{
[ArgShortcut("--str"), ArgShortcut("-s")]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[ArgShortcut("--int"), ArgShortcut("-i")]
public int IntOption { get; set; }

View File

@@ -14,7 +14,7 @@ namespace CliFx.Benchmarks.Commands
{
new Option(new[] {"--str", "-s"})
{
Argument = new Argument<string>()
Argument = new Argument<string?>()
},
new Option(new[] {"--int", "-i"})
{

View File

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

View File

@@ -1,12 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
using CliFx.Demo.Models;
using CliFx.Demo.Services;
using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands
{
@@ -15,31 +13,25 @@ namespace CliFx.Demo.Commands
{
private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
public string Title { get; set; }
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } = "";
[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.")]
public DateTimeOffset Published { get; set; }
public DateTimeOffset Published { get; set; } = CreateRandomDate();
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; set; }
public Isbn Isbn { get; set; } = CreateRandomIsbn();
public BookAddCommand(LibraryService libraryService)
{
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
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)
throw new CommandException("Book already exists.", 1);
@@ -49,7 +41,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine("Book added.");
console.RenderBook(book);
return Task.CompletedTask;
return default;
}
}
@@ -66,7 +58,7 @@ namespace CliFx.Demo.Commands
Random.Next(1, 59),
TimeSpan.Zero);
public static Isbn CreateRandomIsbn() => new Isbn(
private static Isbn CreateRandomIsbn() => new Isbn(
Random.Next(0, 999),
Random.Next(0, 99),
Random.Next(0, 99999),

View File

@@ -1,10 +1,8 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
using CliFx.Demo.Services;
using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands
{
@@ -13,15 +11,15 @@ namespace CliFx.Demo.Commands
{
private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
public string Title { get; set; }
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } = "";
public BookCommand(LibraryService libraryService)
{
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public ValueTask ExecuteAsync(IConsole console)
{
var book = _libraryService.GetBook(Title);
@@ -30,7 +28,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book);
return Task.CompletedTask;
return default;
}
}
}

View File

@@ -1,9 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Internal;
using CliFx.Demo.Services;
using CliFx.Services;
namespace CliFx.Demo.Commands
{
@@ -17,7 +15,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public ValueTask ExecuteAsync(IConsole console)
{
var library = _libraryService.GetLibrary();
@@ -33,7 +31,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book);
}
return Task.CompletedTask;
return default;
}
}
}

View File

@@ -1,9 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Demo.Services;
using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands
{
@@ -12,15 +10,15 @@ namespace CliFx.Demo.Commands
{
private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
public string Title { get; set; }
[CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } = "";
public BookRemoveCommand(LibraryService libraryService)
{
_libraryService = libraryService;
}
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public ValueTask ExecuteAsync(IConsole console)
{
var book = _libraryService.GetBook(Title);
@@ -31,7 +29,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine($"Book {Title} removed.");
return Task.CompletedTask;
return default;
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using System.Globalization;
namespace CliFx.Demo.Models
{
@@ -24,21 +23,23 @@ namespace CliFx.Demo.Models
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 static Isbn Parse(string value)
public static Isbn Parse(string value, IFormatProvider formatProvider)
{
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
return new Isbn(
int.Parse(components[0], CultureInfo.InvariantCulture),
int.Parse(components[1], CultureInfo.InvariantCulture),
int.Parse(components[2], CultureInfo.InvariantCulture),
int.Parse(components[3], CultureInfo.InvariantCulture),
int.Parse(components[4], CultureInfo.InvariantCulture));
int.Parse(components[0], formatProvider),
int.Parse(components[1], formatProvider),
int.Parse(components[2], formatProvider),
int.Parse(components[3], formatProvider),
int.Parse(components[4], formatProvider)
);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using CliFx.Demo.Commands;
using CliFx.Demo.Services;
using Microsoft.Extensions.DependencyInjection;
@@ -7,7 +8,7 @@ namespace CliFx.Demo
{
public static class Program
{
public static Task<int> Main(string[] args)
private static IServiceProvider GetServiceProvider()
{
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
@@ -21,13 +22,14 @@ namespace CliFx.Demo
services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider();
return new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
.Build()
.RunAsync(args);
return services.BuildServiceProvider();
}
public static async Task<int> Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(GetServiceProvider().GetService)
.Build()
.RunAsync();
}
}

View File

@@ -2,6 +2,6 @@
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`.

View File

@@ -25,7 +25,7 @@ namespace CliFx.Demo.Services
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)
{

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

View File

@@ -3,9 +3,8 @@ using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests
@@ -13,109 +12,113 @@ namespace CliFx.Tests
[TestFixture]
public class CliApplicationTests
{
private const string TestAppName = "TestApp";
private const string TestVersionText = "v1.0";
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) },
new[] {typeof(HelloWorldDefaultCommand)},
new string[0],
new Dictionary<string, string>(),
"Hello world."
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "concat", "-i", "foo", "-i", "bar", "-s", " " },
new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
new Dictionary<string, string>(),
"foo bar"
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "concat", "-i", "one", "two", "three", "-s", ", " },
new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
new Dictionary<string, string>(),
"one, two, three"
);
yield return new TestCaseData(
new[] { typeof(DivideCommand) },
new[] { "div", "-D", "24", "-d", "8" },
new[] {typeof(DivideCommand)},
new[] {"div", "-D", "24", "-d", "8"},
new Dictionary<string, string>(),
"3"
);
yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) },
new[] { "--version" },
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "--version" },
new[] {typeof(ConcatCommand)},
new[] {"--version"},
new Dictionary<string, string>(),
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(
new[] { typeof(ConcatCommand) },
new[] {typeof(ConcatCommand)},
new string[0],
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "-h" },
new[] {typeof(ConcatCommand)},
new[] {"-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "--help" },
new[] {typeof(ConcatCommand)},
new[] {"--help"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "concat", "-h" },
new[] {typeof(ConcatCommand)},
new[] {"concat", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ExceptionCommand) },
new[] { "exc", "-h" },
new[] {typeof(ExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) },
new[] { "exc", "-h" },
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "[preview]" },
new[] {typeof(ConcatCommand)},
new[] {"[preview]"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ExceptionCommand) },
new[] { "exc", "[preview]" },
new[] {typeof(ExceptionCommand)},
new[] {"[preview]", "exc"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "concat", "[preview]", "-o", "value" },
new[] {typeof(ConcatCommand)},
new[] {"[preview]", "concat", "-o", "value"},
new Dictionary<string, string>(),
null
);
}
@@ -125,137 +128,324 @@ namespace CliFx.Tests
yield return new TestCaseData(
new Type[0],
new string[0],
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "non-existing" },
new[] {typeof(ConcatCommand)},
new[] {"non-existing"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] { typeof(ExceptionCommand) },
new[] { "exc" },
new[] {typeof(ExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) },
new[] { "exc" },
new[] {typeof(CommandExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) },
new[] { "exc" },
new[] {typeof(CommandExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) },
new[] { "exc", "-m", "foo bar" },
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar"},
new Dictionary<string, string>(),
"foo bar", null
);
yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) },
new[] { "exc", "-m", "foo bar", "-c", "666" },
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar", "-c", "666"},
new Dictionary<string, string>(),
"foo bar", 666
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
string expectedStdOut = null)
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Help()
{
// Arrange
using (var stdoutStream = new StringWriter())
{
var console = new VirtualConsole(stdoutStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
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."
}
);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build();
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."
}
);
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stdOut = stdoutStream.ToString().Trim();
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."
}
);
// Assert
exitCode.Should().Be(0);
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."
}
);
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
else
stdOut.Should().NotBeNullOrWhiteSpace();
}
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]",
}
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
string expectedStdErr = null, int? expectedExitCode = null)
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdOut = null)
{
// Arrange
using (var stderrStream = new StringWriter())
{
var console = new VirtualConsole(TextWriter.Null, stderrStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
await using var stdOutStream = new StringWriter();
var console = new VirtualConsole(stdOutStream);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console)
.Build();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stderr = stderrStream.ToString().Trim();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
// Assert
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
else
exitCode.Should().NotBe(0);
// Assert
exitCode.Should().Be(0);
stdOut.Should().NotBeNullOrWhiteSpace();
if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
else
stderr.Should().NotBeNullOrWhiteSpace();
}
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
Console.WriteLine(stdOut);
}
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdErr = null,
int? expectedExitCode = null)
{
// Arrange
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(TextWriter.Null, stdErrStream);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stderr = stdErrStream.ToString().Trim();
// Assert
exitCode.Should().NotBe(0);
stderr.Should().NotBeNullOrWhiteSpace();
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
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]
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using (var stdoutStream = new StringWriter())
{
var console = new VirtualConsole(stdoutStream);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var args = new[] { "cancel" };
using var cancellationTokenSource = new CancellationTokenSource();
// Act
var runTask = application.RunAsync(args);
console.Cancel();
var exitCode = await runTask.ConfigureAwait(false);
var stdOut = stdoutStream.ToString().Trim();
await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
// Assert
exitCode.Should().Be(-2146233029);
stdOut.Should().Be("Printed");
}
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var commandLineArguments = new[] {"cancel"};
var environmentVariables = new Dictionary<string, string>();
// Act
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
var stdErr = stdErrStream.ToString().Trim();
// Assert
exitCode.Should().NotBe(0);
stdOut.Should().BeNullOrWhiteSpace();
stdErr.Should().NotBeNullOrWhiteSpace();
Console.WriteLine(stdErr);
}
}
}

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
@@ -10,18 +11,21 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.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="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" />
<ProjectReference Include="..\CliFx\CliFx.csproj" />
</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>

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,173 +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;
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;B;C;" }
);
}
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,109 +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,43 +0,0 @@
using System;
using System.IO;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class VirtualConsoleTests
{
// Make sure console uses specified streams and doesn't leak to System.Console
[Test]
public void All_Smoke_Test()
{
// Arrange
using (var stdin = new StringReader("hello world"))
using (var stdout = new StringWriter())
using (var stderr = new StringWriter())
{
var console = new VirtualConsole(stdin, stdout, stderr);
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(stdin);
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
}
}
}

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 CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
namespace CliFx.Tests
{
[TestFixture]
public class SystemConsoleTests
@@ -11,13 +10,12 @@ namespace CliFx.Tests.Services
[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();
}
// Make sure console correctly wraps around System.Console
[Test]
public void All_Smoke_Test()
[Test(Description = "Must be in sync with system console")]
public void Smoke_Test()
{
// Arrange
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,23 +1,16 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public async ValueTask ExecuteAsync(IConsole console)
{
await Task.Yield();
console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Never printed");
}
}
}
}

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -17,10 +15,10 @@ namespace CliFx.Tests.TestCommands
// This property should be ignored by resolver
public bool NotAnOption { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public ValueTask ExecuteAsync(IConsole console)
{
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,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -9,11 +7,11 @@ namespace CliFx.Tests.TestCommands
public class DuplicateOptionNamesCommand : ICommand
{
[CommandOption("fruits")]
public string Apples { get; set; }
public string? Apples { get; set; }
[CommandOption("fruits")]
public string Oranges { get; set; }
public string? Oranges { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,19 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionShortNamesCommand : ICommand
{
[CommandOption('f')]
public string Apples { get; set; }
[CommandOption('f')]
public string Oranges { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
[CommandOption('x')]
public string? OptionA { get; set; }
[CommandOption('x')]
public string? OptionB { 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 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,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -9,8 +7,8 @@ namespace CliFx.Tests.TestCommands
public class EnvironmentVariableCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string Option { get; set; }
public string? Option { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

@@ -1,8 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -10,8 +7,8 @@ namespace CliFx.Tests.TestCommands
public class EnvironmentVariableWithoutCollectionPropertyCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public string Option { get; set; }
public string? Option { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

@@ -1,17 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command]
public class HelloWorldDefaultCommand : ICommand
{
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Hello world.");
return Task.CompletedTask;
return default;
}
}
}

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -9,11 +7,11 @@ namespace CliFx.Tests.TestCommands
public class HelpDefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string OptionA { get; set; }
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string OptionB { get; set; }
public string? OptionB { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -9,11 +7,11 @@ namespace CliFx.Tests.TestCommands
public class HelpNamedCommand : ICommand
{
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string OptionC { get; set; }
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string OptionD { get; set; }
public string? OptionD { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
@@ -9,8 +7,8 @@ namespace CliFx.Tests.TestCommands
public class HelpSubCommand : ICommand
{
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string OptionE { get; set; }
public string? OptionE { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => 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,11 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using CliFx.Services;
using System.Threading.Tasks;
namespace CliFx.Tests.TestCommands
{
public class NonAnnotatedCommand : ICommand
{
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => 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.IO;
using System.Linq;
using CliFx.Services;
using CliFx.Utilities;
using FluentAssertions;
using NUnit.Framework;
@@ -17,41 +16,39 @@ namespace CliFx.Tests.Utilities
// Arrange
var formatProvider = CultureInfo.InvariantCulture;
using (var stdout = new StringWriter(formatProvider))
{
var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
var ticker = console.CreateProgressTicker();
using var stdout = new StringWriter(formatProvider);
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
var ticker = console.CreateProgressTicker();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
// Assert
stdout.ToString().Should().ContainAll(progressStringValues);
}
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert
stdout.ToString().Should().ContainAll(progressStringValues);
}
[Test]
public void Report_Redirected_Test()
{
// Arrange
using (var stdout = new StringWriter())
{
var console = new VirtualConsole(stdout);
var ticker = console.CreateProgressTicker();
using var stdout = new StringWriter();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var console = new VirtualConsole(stdout);
var ticker = console.CreateProgressTicker();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
// Assert
stdout.ToString().Should().BeEmpty();
}
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert
stdout.ToString().Should().BeEmpty();
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.IO;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class VirtualConsoleTests
{
[Test(Description = "Must not leak to system console")]
public void Smoke_Test()
{
// Arrange
using var stdin = new StringReader("hello world");
using var stdout = new StringWriter();
using var stderr = new StringWriter();
var console = new VirtualConsole(stdin, stdout, stderr);
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(stdin);
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
}
}

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
License.txt = License.txt
Readme.md = Readme.md
CliFx.props = CliFx.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

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

View File

@@ -1,6 +1,4 @@
using CliFx.Internal;
namespace CliFx.Models
namespace CliFx
{
/// <summary>
/// Metadata associated with an application.
@@ -25,17 +23,17 @@ namespace CliFx.Models
/// <summary>
/// Application description.
/// </summary>
public string Description { get; }
public string? Description { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata(string title, string executableName, string versionText, string description)
public ApplicationMetadata(string title, string executableName, string versionText, string? description)
{
Title = title.GuardNotNull(nameof(title));
ExecutableName = executableName.GuardNotNull(nameof(executableName));
VersionText = versionText.GuardNotNull(nameof(versionText));
Description = description; // can be null
Title = title;
ExecutableName = executableName;
VersionText = versionText;
Description = description;
}
}
}

View File

@@ -10,27 +10,29 @@ namespace CliFx.Attributes
{
/// <summary>
/// Command name.
/// 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>
public string Name { get; }
public string? Name { get; }
/// <summary>
/// Command description, which is used in help text.
/// </summary>
public string Description { get; set; }
public string? Description { get; set; }
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute"/>.
/// </summary>
public CommandAttribute(string name)
{
Name = name; // can be null
Name = name;
}
/// <summary>
/// Initializes an instance of <see cref="CommandAttribute"/>.
/// </summary>
public CommandAttribute()
: this(null)
{
}
}

View File

@@ -10,11 +10,15 @@ namespace CliFx.Attributes
{
/// <summary>
/// Option name.
/// 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>
public string Name { get; }
public string? Name { get; }
/// <summary>
/// Option short name.
/// 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>
public char? ShortName { get; }
@@ -26,27 +30,27 @@ namespace CliFx.Attributes
/// <summary>
/// Option description, which is used in help text.
/// </summary>
public string Description { get; set; }
public string? Description { get; set; }
/// <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>
public string EnvironmentVariableName { get; set; }
public string? EnvironmentVariableName { get; set; }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(string name, char? shortName)
private CommandOptionAttribute(string? name, char? shortName)
{
Name = name; // can be null
ShortName = shortName; // can be null
Name = name;
ShortName = shortName;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(string name, char shortName)
: this(name, (char?)shortName)
: this(name, (char?) shortName)
{
}
@@ -62,7 +66,7 @@ namespace CliFx.Attributes
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(char shortName)
: this(null, shortName)
: this(null, (char?) shortName)
{
}
}

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,205 +1,162 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Domain;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx
{
/// <summary>
/// Default implementation of <see cref="ICliApplication"/>.
/// Command line application facade.
/// </summary>
public class CliApplication : ICliApplication
public partial class CliApplication
{
private readonly ApplicationMetadata _metadata;
private readonly ApplicationConfiguration _configuration;
private readonly IConsole _console;
private readonly ICommandInputParser _commandInputParser;
private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandFactory _commandFactory;
private readonly ICommandInitializer _commandInitializer;
private readonly IHelpTextRenderer _helpTextRenderer;
private readonly ITypeActivator _typeActivator;
/// <summary>
/// Initializes an instance of <see cref="CliApplication"/>.
/// </summary>
public CliApplication(ApplicationMetadata metadata, ApplicationConfiguration configuration,
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer)
public CliApplication(
ApplicationMetadata metadata, ApplicationConfiguration configuration,
IConsole console, ITypeActivator typeActivator)
{
_metadata = metadata.GuardNotNull(nameof(metadata));
_configuration = configuration.GuardNotNull(nameof(configuration));
_console = console.GuardNotNull(nameof(console));
_commandInputParser = commandInputParser.GuardNotNull(nameof(commandInputParser));
_commandSchemaResolver = commandSchemaResolver.GuardNotNull(nameof(commandSchemaResolver));
_commandFactory = commandFactory.GuardNotNull(nameof(commandFactory));
_commandInitializer = commandInitializer.GuardNotNull(nameof(commandInitializer));
_helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer));
_metadata = metadata;
_configuration = configuration;
_console = console;
_typeActivator = typeActivator;
}
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 && commandInput.IsDebugDirectiveSpecified();
// If not in debug mode, pass execution to the next handler
var isDebugMode = _configuration.IsDebugModeAllowed && commandLineInput.IsDebugDirectiveSpecified;
if (!isDebugMode)
return null;
// Inform user which process they need to attach debugger to
_console.WithForegroundColor(ConsoleColor.Green,
() => _console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
_console.WithForegroundColor(ConsoleColor.Green, () =>
_console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
// Wait until debugger is attached
while (!Debugger.IsAttached)
await Task.Delay(100);
// Debug directive never short-circuits
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 && commandInput.IsPreviewDirectiveSpecified();
// If not in preview mode, pass execution to the next handler
var isPreviewMode = _configuration.IsPreviewModeAllowed && commandLineInput.IsPreviewDirectiveSpecified;
if (!isPreviewMode)
return null;
// Render command name
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
_console.Output.WriteLine();
var commandSchema = applicationSchema.TryFindCommand(commandLineInput, out var argumentOffset);
// Render directives
_console.Output.WriteLine("Directives:");
foreach (var directive in commandInput.Directives)
_console.Output.WriteLine("Parser preview:");
// Command name
if (commandSchema != null && argumentOffset > 0)
{
_console.Output.Write(" ");
_console.Output.WriteLine(directive);
_console.WithForegroundColor(ConsoleColor.Cyan, () =>
_console.Output.Write(commandSchema.Name));
_console.Output.Write(' ');
}
// Margin
_console.Output.WriteLine();
// Render options
_console.Output.WriteLine("Options:");
foreach (var option in commandInput.Options)
// Parameters
foreach (var parameter in commandLineInput.Arguments.Skip(argumentOffset))
{
_console.Output.Write(" ");
_console.Output.WriteLine(option);
_console.Output.Write('<');
_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;
}
private int? HandleVersionOption(CommandInput commandInput)
private int? HandleVersionOption(CommandLineInput commandLineInput)
{
// Version should be rendered if it was requested on a default command
var shouldRenderVersion = !commandInput.IsCommandSpecified() && commandInput.IsVersionOptionSpecified();
// If shouldn't render version, pass execution to the next handler
// Version option is available only on the default command (i.e. when arguments are not specified)
var shouldRenderVersion = !commandLineInput.Arguments.Any() && commandLineInput.IsVersionOptionSpecified;
if (!shouldRenderVersion)
return null;
// Render version text
_console.Output.WriteLine(_metadata.VersionText);
// Short-circuit with exit code 0
return 0;
}
private int? HandleHelpOption(CommandInput commandInput,
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema targetCommandSchema)
private int? HandleHelpOption(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
{
// Help should be rendered if it was requested, or when executing a command which isn't defined
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null;
// Help is rendered either when it's requested or when the user provides no arguments and there is no default command
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)
return null;
// Keep track whether there was an error in the input
var isError = false;
// Get the command schema that matches the input or use a dummy default command as a fallback
var commandSchema =
applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand;
// If target command isn't defined, find its contextual replacement
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."));
RenderHelp(applicationSchema, commandSchema);
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, _console.CancellationToken);
// Finish the chain with exit code 0
return 0;
}
/// <inheritdoc />
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
private async ValueTask<int> HandleCommandExecutionAsync(
ApplicationSchema applicationSchema,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables)
{
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
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
{
// Parse command input from arguments
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments);
var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
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
await HandleDebugDirectiveAsync(commandInput) ??
HandlePreviewDirective(commandInput) ??
HandleVersionOption(commandInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
await HandleCommandExecutionAsync(commandInput, targetCommandSchema);
await HandleDebugDirectiveAsync(commandLineInput) ??
HandlePreviewDirective(applicationSchema, commandLineInput) ??
HandleVersionOption(commandLineInput) ??
HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
}
catch (Exception ex)
{
@@ -207,25 +164,42 @@ namespace CliFx
// 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
if (!ex.Message.IsNullOrWhiteSpace() && (ex is CliFxException || ex is CommandException))
{
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message));
}
else
{
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex));
}
var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
? ex.Message
: ex.ToString();
// Return exit code if it was specified via CommandException
if (ex is CommandException commandException)
{
return commandException.ExitCode;
}
else
{
return ex.HResult;
}
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
return ex is CommandException commandException
? commandException.ExitCode
: 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,146 +3,171 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CliFx.Attributes;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
using CliFx.Domain;
namespace CliFx
{
/// <summary>
/// Default implementation of <see cref="ICliApplicationBuilder"/>.
/// Builds an instance of <see cref="CliApplication"/>.
/// </summary>
public partial class CliApplicationBuilder : ICliApplicationBuilder
public partial class CliApplicationBuilder
{
private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true;
private string _title;
private string _executableName;
private string _versionText;
private string _description;
private IConsole _console;
private ICommandFactory _commandFactory;
private ICommandOptionInputConverter _commandOptionInputConverter;
private IEnvironmentVariablesProvider _environmentVariablesProvider;
private string? _title;
private string? _executableName;
private string? _versionText;
private string? _description;
private IConsole? _console;
private ITypeActivator? _typeActivator;
/// <inheritdoc />
public ICliApplicationBuilder AddCommand(Type commandType)
/// <summary>
/// Adds a command of specified type to the application.
/// </summary>
public CliApplicationBuilder AddCommand(Type commandType)
{
commandType.GuardNotNull(nameof(commandType));
_commandTypes.Add(commandType);
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
/// <summary>
/// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
{
commandAssembly.GuardNotNull(nameof(commandAssembly));
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)
AddCommand(commandType);
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder AllowDebugMode(bool isAllowed = true)
/// <summary>
/// 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;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
/// <summary>
/// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
{
_isPreviewModeAllowed = isAllowed;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseTitle(string title)
/// <summary>
/// Sets application title, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseTitle(string title)
{
_title = title.GuardNotNull(nameof(title));
_title = title;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseExecutableName(string executableName)
/// <summary>
/// Sets application executable name, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseExecutableName(string executableName)
{
_executableName = executableName.GuardNotNull(nameof(executableName));
_executableName = executableName;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseVersionText(string versionText)
/// <summary>
/// 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.GuardNotNull(nameof(versionText));
_versionText = versionText;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseDescription(string description)
/// <summary>
/// Sets application description, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseDescription(string? description)
{
_description = description; // can be null
_description = description;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseConsole(IConsole console)
/// <summary>
/// Configures the application to use the specified implementation of <see cref="IConsole"/>.
/// </summary>
public CliApplicationBuilder UseConsole(IConsole console)
{
_console = console.GuardNotNull(nameof(console));
_console = console;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory)
/// <summary>
/// Configures the application to use the specified implementation of <see cref="ITypeActivator"/>.
/// </summary>
public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator)
{
_commandFactory = factory.GuardNotNull(nameof(factory));
_typeActivator = typeActivator;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
{
_commandOptionInputConverter = converter.GuardNotNull(nameof(converter));
return this;
}
/// <summary>
/// Configures the application to use the specified function for activating types.
/// </summary>
public CliApplicationBuilder UseTypeActivator(Func<Type, object> typeActivator) =>
UseTypeActivator(new DelegateTypeActivator(typeActivator));
/// <inheritdoc />
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider)
/// <summary>
/// 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.GuardNotNull(nameof(environmentVariablesProvider));
return this;
}
_title ??= GetDefaultTitle() ?? "App";
_executableName ??= GetDefaultExecutableName() ?? "app";
_versionText ??= GetDefaultVersionText() ?? "v1.0";
_console ??= new SystemConsole();
_typeActivator ??= new DefaultTypeActivator();
/// <inheritdoc />
public ICliApplication Build()
{
// Use defaults for required parameters that were not configured
_title = _title ?? GetDefaultTitle() ?? "App";
_executableName = _executableName ?? GetDefaultExecutableName() ?? "app";
_versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0";
_console = _console ?? new SystemConsole();
_commandFactory = _commandFactory ?? new CommandFactory();
_commandOptionInputConverter = _commandOptionInputConverter ?? new CommandOptionInputConverter();
_environmentVariablesProvider = _environmentVariablesProvider ?? new EnvironmentVariablesProvider();
// Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration,
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
return new CliApplication(metadata, configuration, _console, _typeActivator);
}
}
@@ -153,9 +178,9 @@ namespace CliFx
// Entry assembly is null in tests
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;
@@ -169,6 +194,9 @@ namespace CliFx
return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
}
private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : null;
private static string? GetDefaultVersionText() =>
EntryAssembly != null
? $"v{EntryAssembly.GetName().Version}"
: null;
}
}

View File

@@ -1,26 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<Version>0.0.6</Version>
<Company>Tyrrrz</Company>
<TargetFrameworks>netstandard2.1;netstandard2.0;net45</TargetFrameworks>
<Authors>$(Company)</Authors>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<Description>Declarative framework for CLI applications</Description>
<PackageTags>command line executable interface framework parser arguments net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
<PackageIcon>favicon.png</PackageIcon>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PublishRepositoryUrl>True</PublishRepositoryUrl>
<EmbedUntrackedSources>True</EmbedUntrackedSources>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" />
</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 CliFx.Internal;
namespace CliFx.Models
namespace CliFx.Domain
{
/// <summary>
/// Parsed option from command line input.
/// </summary>
public partial class CommandOptionInput
internal class CommandOptionInput
{
/// <summary>
/// Specified option alias.
/// </summary>
public string Alias { get; }
/// <summary>
/// Specified values.
/// </summary>
public IReadOnlyList<string> Values { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias, IReadOnlyList<string> values)
{
Alias = alias.GuardNotNull(nameof(alias));
Values = values.GuardNotNull(nameof(values));
Alias = alias;
Values = values;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias, string value)
: this(alias, new[] {value})
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInput"/>.
/// </summary>
public CommandOptionInput(string alias)
: this(alias, EmptyValues)
: this(alias, new string[0])
{
}
/// <inheritdoc />
public override string ToString()
{
var buffer = new StringBuilder();
@@ -70,9 +51,4 @@ namespace CliFx.Models
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

@@ -10,7 +10,7 @@ namespace CliFx.Exceptions
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(string message)
public CliFxException(string? message)
: base(message)
{
}
@@ -18,7 +18,7 @@ namespace CliFx.Exceptions
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(string message, Exception innerException)
public CliFxException(string? message, Exception? innerException)
: base(message, innerException)
{
}

View File

@@ -1,5 +1,4 @@
using System;
using CliFx.Internal;
namespace CliFx.Exceptions
{
@@ -20,16 +19,18 @@ namespace CliFx.Exceptions
/// <summary>
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public CommandException(string message, Exception innerException, int exitCode = DefaultExitCode)
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode)
: base(message, innerException)
{
ExitCode = exitCode.GuardNotZero(nameof(exitCode));
ExitCode = exitCode != 0
? exitCode
: throw new ArgumentException("Exit code must not be zero in order to signify failure.");
}
/// <summary>
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public CommandException(string message, int exitCode = DefaultExitCode)
public CommandException(string? message, int exitCode = DefaultExitCode)
: this(message, null, exitCode)
{
}

View File

@@ -1,63 +1,42 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx
{
/// <summary>
/// Extensions for <see cref="CliFx"/>.
/// Extensions for <see cref="CliFx"/>
/// </summary>
public static class Extensions
{
/// <summary>
/// Adds multiple commands to the application.
/// Sets console foreground color, executes specified action, and sets the color back to the original value.
/// </summary>
public static ICliApplicationBuilder AddCommands(this ICliApplicationBuilder builder, IReadOnlyList<Type> commandTypes)
public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
{
builder.GuardNotNull(nameof(builder));
commandTypes.GuardNotNull(nameof(commandTypes));
var lastColor = console.ForegroundColor;
console.ForegroundColor = foregroundColor;
foreach (var commandType in commandTypes)
builder.AddCommand(commandType);
action();
return builder;
console.ForegroundColor = lastColor;
}
/// <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>
public static ICliApplicationBuilder AddCommandsFrom(this ICliApplicationBuilder builder, IReadOnlyList<Assembly> commandAssemblies)
public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
{
builder.GuardNotNull(nameof(builder));
commandAssemblies.GuardNotNull(nameof(commandAssemblies));
var lastColor = console.BackgroundColor;
console.BackgroundColor = backgroundColor;
foreach (var commandAssembly in commandAssemblies)
builder.AddCommandsFrom(commandAssembly);
action();
return builder;
console.BackgroundColor = lastColor;
}
/// <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>
public static ICliApplicationBuilder AddCommandsFromThisAssembly(this ICliApplicationBuilder builder)
{
builder.GuardNotNull(nameof(builder));
return builder.AddCommandsFrom(Assembly.GetCallingAssembly());
}
/// <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.GuardNotNull(nameof(builder));
factoryMethod.GuardNotNull(nameof(factoryMethod));
return builder.UseCommandFactory(new DelegateCommandFactory(factoryMethod));
}
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action) =>
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
}
}

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

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