63 Commits
0.0.3 ... 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
Alexey Golub
9564cd5d30 Update version 2019-10-30 18:41:24 +02:00
Moophic
ed458c3980 Cancellation support (#28) 2019-10-30 18:37:32 +02:00
Alexey Golub
25538f99db Migrate from PackageIconUrl to PackageIcon 2019-10-08 16:59:13 +03:00
Federico Paolillo
36436e7a4b Environment variables (#27) 2019-09-29 20:44:24 +03:00
Alexey Golub
a6070332c9 Migrate to .NET Core 3 where applicable 2019-09-25 22:52:33 +03:00
Alexey Golub
25cbfdb4b8 Move screenshots to repository 2019-09-06 20:24:28 +03:00
Alexey Golub
d1b5107c2c Update version 2019-08-26 20:48:43 +03:00
Alexey Golub
03873d63cd Fix exception when converting option values to array when there's only one value 2019-08-26 20:47:23 +03:00
Alexey Golub
89aba39964 Add extensibility point for injecting custom option converters
Closes #19
2019-08-26 20:10:37 +03:00
Alexey Golub
ab57a103d1 Update benchmarks 2019-08-26 17:20:14 +03:00
Alexey Golub
d0b2ebc061 Update readme 2019-08-25 23:27:19 +03:00
Alexey Golub
857257ca73 Update version 2019-08-25 23:19:10 +03:00
Alexey Golub
3587155c7e Update readme 2019-08-25 23:17:58 +03:00
Alexey Golub
ae05e0db96 Refactor 2019-08-25 22:08:34 +03:00
Alexey Golub
41c0493e66 Refactor tests again 2019-08-25 18:26:40 +03:00
Alexey Golub
43a304bb26 Refactor tests 2019-08-25 17:28:54 +03:00
Alexey Golub
cd3892bf83 Refactor CliApplication.RunAsync using chain of responsibility 2019-08-25 14:54:29 +03:00
Alexey Golub
3f7c02342d Add smoke tests for VirtualConsole 2019-08-25 11:30:06 +03:00
Alexey Golub
c65cdf465e Remove dummy tests 2019-08-24 23:25:41 +03:00
Alexey Golub
b5d67ecf24 Fix not printing version when requested if used with stub default command 2019-08-24 22:46:10 +03:00
Alexey Golub
a94b2296e1 Add tests for CommandInitializer that verify that short name comparison is case sensitive 2019-08-24 22:44:11 +03:00
Alexey Golub
fa05e4df3f Rework schema validation in CommandSchemaResolver 2019-08-24 22:23:12 +03:00
Alexey Golub
b70b25076e Add smoke tests for CliApplicationBuilder 2019-08-24 18:31:17 +03:00
Alexey Golub
0662f341e6 Rename some methods 2019-08-24 18:25:56 +03:00
Alexey Golub
80bf477f3b Add support for directives (debug and preview)
Closes #7
Closes #8
2019-08-24 18:22:54 +03:00
Alexey Golub
e4a502d9d6 Rename ProgressReporter to ProgressTicker 2019-08-24 13:00:13 +03:00
Alexey Golub
13b15b98ed Add ProgressReporter
Closes #14
2019-08-23 22:50:43 +03:00
Alexey Golub
80465e0e51 Move tests into corresponding namespaces 2019-08-23 17:01:49 +03:00
Alexey Golub
9a1ce7e7e5 Add 1 more negative test for CommandSchemaResolver 2019-08-22 12:08:08 +03:00
Alexey Golub
b45da64664 Make CommandAttribute non-optional on command types 2019-08-21 21:04:42 +03:00
Alexey Golub
df01dc055e Prepend 'v' to default version text 2019-08-21 15:55:05 +03:00
Alexey Golub
31dd24d189 Sort options when rendering help 2019-08-21 14:37:53 +03:00
148 changed files with 5200 additions and 4457 deletions

3
.github/FUNDING.yml vendored
View File

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

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

1
.gitignore vendored
View File

@@ -143,6 +143,7 @@ _TeamCity*
_NCrunch_* _NCrunch_*
.*crunch*.local.xml .*crunch*.local.xml
nCrunchTemp_* nCrunchTemp_*
.ncrunchsolution
# MightyMoose # MightyMoose
*.mm.* *.mm.*

BIN
.screenshots/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,34 +1,46 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using CliFx.Benchmarks.Commands; using CliFx.Benchmarks.Commands;
using CommandLine;
namespace CliFx.Benchmarks namespace CliFx.Benchmarks
{ {
[CoreJob] [SimpleJob]
[RankColumn] [RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Benchmark public class Benchmark
{ {
private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[Benchmark(Description = "CliFx", Baseline = true)] [Benchmark(Description = "CliFx", Baseline = true)]
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); public async ValueTask<int> ExecuteWithCliFx() =>
await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
[Benchmark(Description = "System.CommandLine")] [Benchmark(Description = "System.CommandLine")]
public Task<int> ExecuteWithSystemCommandLine() => new SystemCommandLineCommand().ExecuteAsync(Arguments); public async Task<int> ExecuteWithSystemCommandLine() =>
await new SystemCommandLineCommand().ExecuteAsync(Arguments);
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments); public int ExecuteWithMcMaster() =>
McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
// Skipped because this benchmark freezes after a couple of iterations [Benchmark(Description = "CommandLineParser")]
// Probably wasn't designed to run multiple times in single process execution public void ExecuteWithCommandLineParser() =>
//[Benchmark(Description = "CommandLineParser")] new Parser()
public void ExecuteWithCommandLineParser() .ParseArguments(Arguments, typeof(CommandLineParserCommand))
{ .WithParsed<CommandLineParserCommand>(c => c.Execute());
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand));
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
}
[Benchmark(Description = "PowerArgs")] [Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments); public void ExecuteWithPowerArgs() =>
PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
[Benchmark(Description = "Clipr")]
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"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> <PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="clipr" Version="1.6.1" />
<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="PowerArgs" Version="3.6.0" />
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" /> <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
</ItemGroup> </ItemGroup>

View File

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

View File

@@ -0,0 +1,20 @@
using clipr;
namespace CliFx.Benchmarks.Commands
{
public class CliprCommand
{
[NamedArgument('s', "str")]
public string? StrOption { get; set; }
[NamedArgument('i', "int")]
public int IntOption { get; set; }
[NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
public bool BoolOption { get; set; }
public void Execute()
{
}
}
}

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 public class CommandLineParserCommand
{ {
[Option('s', "str")] [Option('s', "str")]
public string StrOption { get; set; } public string? StrOption { get; set; }
[Option('i', "int")] [Option('i', "int")]
public int IntOption { get; set; } public int IntOption { get; set; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using CliFx.Demo.Commands; using CliFx.Demo.Commands;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -7,7 +8,7 @@ namespace CliFx.Demo
{ {
public static class Program 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 // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -21,13 +22,14 @@ namespace CliFx.Demo
services.AddTransient<BookRemoveCommand>(); services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>(); services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider(); return services.BuildServiceProvider();
return new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
.Build()
.RunAsync(args);
} }
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. Sample command line interface for managing a library of books.
This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things. This demo project shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things.
You can get a list of available commands by running `CliFx.Demo --help`. You can get a list of available commands by running `CliFx.Demo --help`.

View File

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

View File

@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net46</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Version>1.2.3.4</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,31 +0,0 @@
using System.Text;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command]
public class GreeterCommand : ICommand
{
[CommandOption("target", 't', Description = "Greeting target.")]
public string Target { get; set; } = "world";
[CommandOption('e', Description = "Whether the greeting should be exclaimed.")]
public bool IsExclaimed { get; set; }
public Task ExecuteAsync(IConsole console)
{
var buffer = new StringBuilder();
buffer.Append("Hello").Append(' ').Append(Target);
if (IsExclaimed)
buffer.Append('!');
console.Output.WriteLine(buffer.ToString());
return Task.CompletedTask;
}
}
}

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

@@ -1,25 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command("log", Description = "Calculate the logarithm of a value.")]
public class LogCommand : ICommand
{
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
public double Value { get; set; }
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; set; } = 10;
public Task ExecuteAsync(IConsole console)
{
var result = Math.Log(Value, Base);
console.Output.WriteLine(result);
return Task.CompletedTask;
}
}
}

View File

@@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command("sum", Description = "Calculate the sum of all input values.")]
public class SumCommand : ICommand
{
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
public IReadOnlyList<double> Values { get; set; }
public Task ExecuteAsync(IConsole console)
{
var result = Values.Sum();
console.Output.WriteLine(result);
return Task.CompletedTask;
}
}
}

View File

@@ -1,21 +1,13 @@
using System.Globalization; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace CliFx.Tests.Dummy namespace CliFx.Tests.Dummy
{ {
public static class Program public class Program
{ {
public static Task<int> Main(string[] args) public static async Task Main() =>
{ await new CliApplicationBuilder()
// Set culture to invariant to maintain consistent format because we rely on it in tests
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
return new CliApplicationBuilder()
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseDescription("Dummy program used for E2E tests.")
.Build() .Build()
.RunAsync(args); .RunAsync();
}
} }
} }

View File

@@ -0,0 +1,45 @@
using NUnit.Framework;
using System;
using System.IO;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests
{
[TestFixture]
public class CliApplicationBuilderTests
{
[Test(Description = "All builder methods must return without exceptions")]
public void Smoke_Test()
{
// Arrange
var builder = new CliApplicationBuilder();
// Act
builder
.AddCommand(typeof(HelloWorldDefaultCommand))
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
.AddCommandsFromThisAssembly()
.AllowDebugMode()
.AllowPreviewMode()
.UseTitle("test")
.UseExecutableName("test")
.UseVersionText("test")
.UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null))
.UseTypeActivator(Activator.CreateInstance)
.Build();
}
[Test(Description = "Builder must be able to produce an application when no parameters are specified")]
public void Build_Test()
{
// Arrange
var builder = new CliApplicationBuilder();
// Act
builder.Build();
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CliApplicationTests
{
[Command]
private class DefaultCommand : ICommand
{
public Task ExecuteAsync(IConsole console)
{
console.Output.WriteLine("DefaultCommand executed.");
return Task.CompletedTask;
}
}
[Command("cmd")]
private class NamedCommand : ICommand
{
public Task ExecuteAsync(IConsole console)
{
console.Output.WriteLine("NamedCommand executed.");
return Task.CompletedTask;
}
}
}
// Negative
public partial class CliApplicationTests
{
[Command("faulty1")]
private class FaultyCommand1 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new CommandException(150);
}
[Command("faulty2")]
private class FaultyCommand2 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new CommandException("FaultyCommand2 error message.", 150);
}
[Command("faulty3")]
private class FaultyCommand3 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new Exception("FaultyCommand3 error message.");
}
}
}

View File

@@ -1,76 +1,125 @@
using System; using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services; using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests namespace CliFx.Tests
{ {
[TestFixture] [TestFixture]
public partial class CliApplicationTests public class CliApplicationTests
{ {
private const string TestAppName = "TestApp";
private const string TestVersionText = "v1.0";
private static IEnumerable<TestCaseData> GetTestCases_RunAsync() private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new string[0], new string[0],
"DefaultCommand executed." new Dictionary<string, string>(),
"Hello world."
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(NamedCommand)}, new[] {typeof(ConcatCommand)},
new[] {"cmd"}, new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
"NamedCommand executed." new Dictionary<string, string>(),
); "foo bar"
}
private static IEnumerable<TestCaseData> GetTestCases_HelpAndVersion_RunAsync()
{
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new string[0]
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DefaultCommand)}, new[] {typeof(ConcatCommand)},
new[] {"-h"} new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
new Dictionary<string, string>(),
"one, two, three"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DefaultCommand)}, new[] {typeof(DivideCommand)},
new[] {"--help"} new[] {"div", "-D", "24", "-d", "8"},
new Dictionary<string, string>(),
"3"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--version"} new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(NamedCommand)}, new[] {typeof(ConcatCommand)},
new string[0] new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(NamedCommand)}, new[] {typeof(ConcatCommand)},
new[] {"cmd", "-h"} new string[0],
new Dictionary<string, string>(),
null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand1)}, new[] {typeof(ConcatCommand)},
new[] {"faulty1", "-h"} new[] {"-h"},
new Dictionary<string, string>(),
null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand2)}, new[] {typeof(ConcatCommand)},
new[] {"faulty2", "-h"} new[] {"--help"},
new Dictionary<string, string>(),
null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand3)}, new[] {typeof(ConcatCommand)},
new[] {"faulty3", "-h"} new[] {"concat", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"[preview]"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ExceptionCommand)},
new[] {"[preview]", "exc"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"[preview]", "concat", "-o", "value"},
new Dictionary<string, string>(),
null
); );
} }
@@ -78,97 +127,325 @@ namespace CliFx.Tests
{ {
yield return new TestCaseData( yield return new TestCaseData(
new Type[0], new Type[0],
new string[0] new string[0],
new Dictionary<string, string>(),
null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DefaultCommand)}, new[] {typeof(ConcatCommand)},
new[] {"non-existing"} new[] {"non-existing"},
new Dictionary<string, string>(),
null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand1)}, new[] {typeof(ExceptionCommand)},
new[] {"faulty1"} new[] {"exc"},
new Dictionary<string, string>(),
null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand2)}, new[] {typeof(CommandExceptionCommand)},
new[] {"faulty2"} new[] {"exc"},
new Dictionary<string, string>(),
null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(FaultyCommand3)}, new[] {typeof(CommandExceptionCommand)},
new[] {"faulty3"} new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
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 Dictionary<string, string>(),
"foo bar", 666
);
}
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Help()
{
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Description",
"HelpDefaultCommand description.",
"Usage",
TestAppName, "[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "HelpNamedCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Usage",
TestAppName, "[command]",
"Options",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "--help"},
new[]
{
"Description",
"HelpNamedCommand description.",
"Usage",
TestAppName, "cmd", "[command]", "[options]",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "sub", "--help"},
new[]
{
"Description",
"HelpSubCommand description.",
"Usage",
TestAppName, "cmd sub", "[options]",
"Options",
"-e|--option-e", "OptionE description.",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new[] {"param", "cmd", "--help"},
new[]
{
"Description",
"Command using positional parameters",
"Usage",
TestAppName, "param cmd", "<first>", "<parameterb>", "<third list...>", "[options]",
"Parameters",
"* first",
"* parameterb",
"* third list", "A list of numbers",
"Options",
"-o|--option",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(AllRequiredOptionsCommand)},
new[] {"allrequired", "--help"},
new[]
{
"Description",
"AllRequiredOptionsCommand description.",
"Usage",
TestAppName, "allrequired --option-f <value> --option-g <value>"
}
);
yield return new TestCaseData(
new[] {typeof(SomeRequiredOptionsCommand)},
new[] {"somerequired", "--help"},
new[]
{
"Description",
"SomeRequiredOptionsCommand description.",
"Usage",
TestAppName, "somerequired --option-f <value> [options]"
}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
new[] {"--help"},
new[]
{
"Environment variable:", "ENV_SINGLE_VALUE"
}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "--help"},
new[]
{
"Usage",
TestAppName, "concat", "-i", "<values...>", "[options]",
}
); );
} }
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync))] [TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, string expectedStdOut) public async Task RunAsync_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdOut = null)
{ {
// Arrange // Arrange
using (var stdout = new StringWriter()) await using var stdOutStream = new StringWriter();
{ var console = new VirtualConsole(stdOutStream);
var console = new VirtualConsole(stdout);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseConsole(console) .UseTitle(TestAppName)
.Build(); .UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = stdOutStream.ToString().Trim();
// Assert // Assert
exitCode.Should().Be(0); exitCode.Should().Be(0);
stdout.ToString().Trim().Should().Be(expectedStdOut); stdOut.Should().NotBeNullOrWhiteSpace();
}
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
Console.WriteLine(stdOut);
} }
[Test]
[TestCaseSource(nameof(GetTestCases_HelpAndVersion_RunAsync))]
public async Task RunAsync_HelpAndVersion_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
{
// Arrange
using (var stdout = new StringWriter())
{
var console = new VirtualConsole(stdout);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
// Assert
exitCode.Should().Be(0);
stdout.ToString().Should().NotBeNullOrWhiteSpace();
}
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments) public async Task RunAsync_Negative_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdErr = null,
int? expectedExitCode = null)
{ {
// Arrange // Arrange
using (var stderr = new StringWriter()) await using var stdErrStream = new StringWriter();
{ var console = new VirtualConsole(TextWriter.Null, stdErrStream);
var console = new VirtualConsole(TextWriter.Null, stderr);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseConsole(console) .UseTitle(TestAppName)
.Build(); .UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act // Act
var exitCode = await application.RunAsync(commandLineArguments); var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stderr = stdErrStream.ToString().Trim();
// Assert // Assert
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
stderr.ToString().Should().NotBeNullOrWhiteSpace(); 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 cancellationTokenSource = new CancellationTokenSource();
await using var stdOutStream = new StringWriter();
await using var stdErrStream = new StringWriter();
var console = new VirtualConsole(stdOutStream, stdErrStream, cancellationTokenSource.Token);
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,25 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>net46</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat> <CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput> <CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.8.0" /> <PackageReference Include="CliWrap" Version="2.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.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="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="CliWrap" Version="2.3.1" /> <PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -27,4 +24,8 @@
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="Copy dummy's runtime config" AfterTargets="AfterBuild">
<Copy SourceFiles="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json" DestinationFiles="$(OutputPath)CliFx.Tests.Dummy.runtimeconfig.json" />
</Target>
</Project> </Project>

View File

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandFactoryTests
{
[Command]
private class TestCommand : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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(TestCommand)));
}
[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,21 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandInitializerTests
{
[Command]
private class TestCommand : ICommand
{
[CommandOption("int", 'i', IsRequired = true)]
public int IntOption { get; set; } = 24;
[CommandOption("str", 's')]
public string StringOption { get; set; } = "foo bar";
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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 TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("int", "13")
}),
new TestCommand {IntOption = 13}
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("int", "13"),
new CommandOptionInput("str", "hello world")
}),
new TestCommand {IntOption = 13, StringOption = "hello world"}
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("i", "13")
}),
new TestCommand {IntOption = 13}
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
{
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
CommandInput.Empty
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("str", "hello world")
})
);
}
[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<MissingCommandOptionInputException>();
}
}
}

View File

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

View File

@@ -1,63 +0,0 @@
using System;
namespace CliFx.Tests
{
public partial class CommandOptionInputConverterTests
{
private enum TestEnum
{
Value1,
Value2,
Value3
}
private class TestStringConstructable
{
public string Value { get; }
public TestStringConstructable(string value)
{
Value = value;
}
}
private class TestStringParseable
{
public string Value { get; }
private TestStringParseable(string value)
{
Value = value;
}
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
}
private class TestStringParseableWithFormatProvider
{
public string Value { get; }
private TestStringParseableWithFormatProvider(string value)
{
Value = value;
}
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
}
}
// Negative
public partial class CommandOptionInputConverterTests
{
private class NonStringParseable
{
public int Value { get; }
public NonStringParseable(int value)
{
Value = value;
}
}
}
}

View File

@@ -1,305 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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[] {"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", "123"),
typeof(NonStringParseable)
);
}
[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<InvalidCommandOptionInputException>();
}
}
}

View File

@@ -1,81 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandSchemaResolverTests
{
[Command("cmd", Description = "NormalCommand1 description.")]
private class NormalCommand1 : ICommand
{
[CommandOption("option-a", 'a')]
public int OptionA { get; set; }
[CommandOption("option-b", IsRequired = true)]
public string OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command(Description = "NormalCommand2 description.")]
private class NormalCommand2 : ICommand
{
[CommandOption("option-c", Description = "OptionC description.")]
public bool OptionC { get; set; }
[CommandOption("option-d", 'd')]
public DateTimeOffset OptionD { get; set; }
public string NotAnOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
// Negative
public partial class CommandSchemaResolverTests
{
[Command("conflict")]
private class ConflictingCommand1 : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command("conflict")]
private class ConflictingCommand2 : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command]
private class InvalidCommand1
{
}
[Command]
private class InvalidCommand2 : ICommand
{
[CommandOption("conflict")]
public string ConflictingOption1 { get; set; }
[CommandOption("conflict")]
public string ConflictingOption2 { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command]
private class InvalidCommand3 : ICommand
{
[CommandOption('c')]
public string ConflictingOption1 { get; set; }
[CommandOption('c')]
public string ConflictingOption2 { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial class CommandSchemaResolverTests
{
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
{
yield return new TestCaseData(
new[] {typeof(NormalCommand1), typeof(NormalCommand2)},
new[]
{
new CommandSchema(typeof(NormalCommand1), "cmd", "NormalCommand1 description.",
new[]
{
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionA)),
"option-a", 'a', false, null),
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionB)),
"option-b", null, true, null)
}),
new CommandSchema(typeof(NormalCommand2), null, "NormalCommand2 description.",
new[]
{
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionC)),
"option-c", null, false, "OptionC description."),
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionD)),
"option-d", 'd', false, null)
})
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
{
yield return new TestCaseData(new object[]
{
new Type[0]
});
yield return new TestCaseData(new object[]
{
new[] {typeof(ConflictingCommand1), typeof(ConflictingCommand2)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand1)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand2)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand3)}
});
}
[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<InvalidCommandSchemaException>();
}
}
}

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

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class DelegateCommandFactoryTests
{
[Command]
private class TestCommand : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,39 +1,53 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using CliFx.Exceptions;
using CliFx.Models; using CliFx.Tests.TestCommands;
using CliFx.Services;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
namespace CliFx.Tests namespace CliFx.Tests
{ {
[TestFixture] [TestFixture]
public partial class DelegateCommandFactoryTests public class DelegateCommandFactoryTests
{ {
private static CommandSchema GetCommandSchema(Type commandType) => private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)), new Func<Type, object>(Activator.CreateInstance),
GetCommandSchema(typeof(TestCommand)) typeof(HelloWorldDefaultCommand)
); );
} }
[Test] private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
[TestCaseSource(nameof(GetTestCases_CreateCommand))] {
public void CreateCommand_Test(Func<CommandSchema, ICommand> factoryMethod, CommandSchema commandSchema) 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 // Arrange
var factory = new DelegateCommandFactory(factoryMethod); var activator = new DelegateTypeActivator(activatorFunc);
// Act // Act
var command = factory.CreateCommand(commandSchema); var obj = activator.CreateInstance(type);
// Assert // Assert
command.Should().BeOfType(commandSchema.Type); 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);
}
}
}

View File

@@ -1,4 +1,8 @@
using System.Threading.Tasks; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CliWrap; using CliWrap;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@@ -8,66 +12,71 @@ namespace CliFx.Tests
[TestFixture] [TestFixture]
public class DummyTests public class DummyTests
{ {
private static string DummyFilePath => typeof(Dummy.Program).Assembly.Location; private static Assembly DummyAssembly { get; } = typeof(Dummy.Program).Assembly;
private static string DummyVersionText => typeof(Dummy.Program).Assembly.GetName().Version.ToString(); private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
[Test]
[TestCase("", "Hello world")]
[TestCase("-t .NET", "Hello .NET")]
[TestCase("-e", "Hello world!")]
[TestCase("sum -v 1 2", "3")]
[TestCase("sum -v 2.75 3.6 4.18", "10.53")]
[TestCase("sum -v 4 -v 16", "20")]
[TestCase("sum --values 2 5 --values 3", "10")]
[TestCase("log -v 100", "2")]
[TestCase("log --value 256 --base 2", "8")]
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
{ {
// Arrange & Act yield return new TestCaseData(
var result = await Cli.Wrap(DummyFilePath) new[] {"--version"},
.SetArguments(arguments) new Dictionary<string, string>(),
.EnableExitCodeValidation() $"v{DummyAssembly.GetName().Version}"
.EnableStandardErrorValidation() );
.ExecuteAsync();
// Assert yield return new TestCaseData(
result.StandardOutput.Trim().Should().Be(expectedOutput); 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!"
);
} }
[Test] [TestCaseSource(nameof(GetTestCases_RunAsync))]
[TestCase("--version")] public async Task RunAsync_Test(
public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments) IReadOnlyList<string> arguments,
IReadOnlyDictionary<string, string> environmentVariables,
string expectedStdOut)
{ {
// Arrange & Act // Arrange
var result = await Cli.Wrap(DummyFilePath) var cli = Cli.Wrap("dotnet")
.SetArguments(arguments) .SetArguments(arguments.Prepend(DummyAssembly.Location).ToArray())
.EnableExitCodeValidation() .EnableExitCodeValidation()
.EnableStandardErrorValidation() .EnableStandardErrorValidation()
.ExecuteAsync(); .SetStandardOutputCallback(Console.WriteLine)
.SetStandardErrorCallback(Console.WriteLine);
foreach (var (key, value) in environmentVariables)
cli.SetEnvironmentVariable(key, value);
// Act
var result = await cli.ExecuteAsync();
// Assert // Assert
result.StandardOutput.Trim().Should().Be(DummyVersionText); result.ExitCode.Should().Be(0);
} result.StandardError.Should().BeNullOrWhiteSpace();
result.StandardOutput.TrimEnd().Should().Be(expectedStdOut);
[Test]
[TestCase("--help")]
[TestCase("-h")]
[TestCase("sum -h")]
[TestCase("sum --help")]
[TestCase("log -h")]
[TestCase("log --help")]
public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments)
{
// Arrange & Act
var result = await Cli.Wrap(DummyFilePath)
.SetArguments(arguments)
.EnableExitCodeValidation()
.EnableStandardErrorValidation()
.ExecuteAsync();
// Assert
result.StandardOutput.Trim().Should().NotBeNullOrWhiteSpace();
} }
} }
} }

View File

@@ -1,42 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class HelpTextRendererTests
{
[Command(Description = "DefaultCommand description.")]
private class DefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command("cmd", Description = "NamedCommand description.")]
private class NamedCommand : ICommand
{
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string OptionD { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command("cmd sub", Description = "NamedSubCommand description.")]
private class NamedSubCommand : ICommand
{
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string OptionE { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
typeof(DefaultCommand)),
new[]
{
"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", "NamedCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
typeof(NamedCommand)),
new[]
{
"Description",
"NamedCommand description.",
"Usage",
"cmd", "[command]", "[options]",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "NamedSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
typeof(NamedSubCommand)),
new[]
{
"Description",
"NamedSubCommand 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 renderer = new HelpTextRenderer();
var console = new VirtualConsole(stdout);
// Act
renderer.RenderHelpText(console, source);
// Assert
stdout.ToString().Should().ContainAll(expectedSubstrings);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class SystemConsoleTests
{
[TearDown]
public void TearDown()
{
// Reset console color so it doesn't carry on into the next tests
Console.ResetColor();
}
[Test(Description = "Must be in sync with system console")]
public void Smoke_Test()
{
// Arrange
var console = new SystemConsole();
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(Console.In);
console.IsInputRedirected.Should().Be(Console.IsInputRedirected);
console.Output.Should().BeSameAs(Console.Out);
console.IsOutputRedirected.Should().Be(Console.IsOutputRedirected);
console.Error.Should().BeSameAs(Console.Error);
console.IsErrorRedirected.Should().Be(Console.IsErrorRedirected);
console.ForegroundColor.Should().Be(Console.ForegroundColor);
console.BackgroundColor.Should().Be(Console.BackgroundColor);
}
}
}

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

@@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async ValueTask ExecuteAsync(IConsole console)
{
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Never printed");
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
namespace CliFx.Tests.TestCommands
{
[Command("exc")]
public class CommandExceptionCommand : ICommand
{
[CommandOption("code", 'c')]
public int ExitCode { get; set; } = 1337;
[CommandOption("msg", 'm')]
public string? Message { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("concat", Description = "Concatenate strings.")]
public class ConcatCommand : ICommand
{
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
public IReadOnlyList<string> Inputs { get; set; }
[CommandOption('s', Description = "String separator.")]
public string Separator { get; set; } = "";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(string.Join(Separator, Inputs));
return default;
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("div", Description = "Divide one number by another.")]
public class DivideCommand : ICommand
{
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
public double Dividend { get; set; }
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
public double Divisor { get; set; }
// This property should be ignored by resolver
public bool NotAnOption { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Dividend / Divisor);
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

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionNamesCommand : ICommand
{
[CommandOption("fruits")]
public string? Apples { get; set; }
[CommandOption("fruits")]
public string? Oranges { 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 DuplicateOptionShortNamesCommand : ICommand
{
[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

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads option values from environment variables.")]
public class EnvironmentVariableCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads multiple option values from environment variables.")]
public class EnvironmentVariableWithMultipleValuesCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public IEnumerable<string>? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads one option value from environment variables because target property is not a collection.")]
public class EnvironmentVariableWithoutCollectionPropertyCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "HelpDefaultCommand description.")]
public class HelpDefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
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("cmd", Description = "HelpNamedCommand description.")]
public class HelpNamedCommand : ICommand
{
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string? OptionD { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("cmd sub", Description = "HelpSubCommand description.")]
public class HelpSubCommand : ICommand
{
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string? OptionE { get; set; }
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

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

View File

@@ -0,0 +1,9 @@
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class NonImplementedCommand
{
}
}

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

@@ -0,0 +1,9 @@
namespace CliFx.Tests.TestCustomTypes
{
public enum TestEnum
{
Value1,
Value2,
Value3
}
}

View File

@@ -0,0 +1,12 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestNonStringParseable
{
public int Value { get; }
public TestNonStringParseable(int value)
{
Value = value;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringConstructable
{
public string Value { get; }
public TestStringConstructable(string value)
{
Value = value;
}
}
}

View File

@@ -0,0 +1,14 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringParseable
{
public string Value { get; }
private TestStringParseable(string value)
{
Value = value;
}
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringParseableWithFormatProvider
{
public string Value { get; }
private TestStringParseableWithFormatProvider(string value)
{
Value = value;
}
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
}
}

View File

@@ -0,0 +1,54 @@
using System.Globalization;
using System.IO;
using System.Linq;
using CliFx.Utilities;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Utilities
{
[TestFixture]
public class ProgressTickerTests
{
[Test]
public void Report_Test()
{
// 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();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
// 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();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
// 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

@@ -7,18 +7,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
Changelog.md = Changelog.md Changelog.md = Changelog.md
License.txt = License.txt License.txt = License.txt
Readme.md = Readme.md Readme.md = Readme.md
CliFx.props = CliFx.props
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -54,18 +55,6 @@ Global
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = Release|Any CPU
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.Build.0 = Debug|Any CPU {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU {8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -90,6 +79,18 @@ Global
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU {AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.ActiveCfg = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.Build.0 = Debug|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.Build.0 = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.ActiveCfg = Release|Any CPU
{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace CliFx
{
/// <summary>
/// Configuration of an application.
/// </summary>
public class ApplicationConfiguration
{
/// <summary>
/// Command types defined in this application.
/// </summary>
public IReadOnlyList<Type> CommandTypes { get; }
/// <summary>
/// Whether debug mode is allowed in this application.
/// </summary>
public bool IsDebugModeAllowed { get; }
/// <summary>
/// Whether preview mode is allowed in this application.
/// </summary>
public bool IsPreviewModeAllowed { get; }
/// <summary>
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
/// </summary>
public ApplicationConfiguration(
IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed, bool isPreviewModeAllowed)
{
CommandTypes = commandTypes;
IsDebugModeAllowed = isDebugModeAllowed;
IsPreviewModeAllowed = isPreviewModeAllowed;
}
}
}

View File

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

View File

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

View File

@@ -10,11 +10,15 @@ namespace CliFx.Attributes
{ {
/// <summary> /// <summary>
/// Option name. /// 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> /// </summary>
public string Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// Option short name. /// Option short name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// All options in a command must have different short names (comparison is case-sensitive).
/// </summary> /// </summary>
public char? ShortName { get; } public char? ShortName { get; }
@@ -26,15 +30,20 @@ namespace CliFx.Attributes
/// <summary> /// <summary>
/// Option description, which is used in help text. /// Option description, which is used in help text.
/// </summary> /// </summary>
public string Description { get; set; } public string? Description { get; set; }
/// <summary>
/// Environment variable that will be used as fallback if no option value is specified.
/// </summary>
public string? EnvironmentVariableName { get; set; }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(string name, char? shortName) private CommandOptionAttribute(string? name, char? shortName)
{ {
Name = name; // can be null Name = name;
ShortName = shortName; // can be null ShortName = shortName;
} }
/// <summary> /// <summary>
@@ -57,7 +66,7 @@ namespace CliFx.Attributes
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(char shortName) 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,144 +1,205 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplication"/>. /// Command line application facade.
/// </summary> /// </summary>
public class CliApplication : ICliApplication public partial class CliApplication
{ {
private readonly ApplicationMetadata _metadata; private readonly ApplicationMetadata _metadata;
private readonly ApplicationConfiguration _configuration; private readonly ApplicationConfiguration _configuration;
private readonly IConsole _console; private readonly IConsole _console;
private readonly ICommandInputParser _commandInputParser; private readonly ITypeActivator _typeActivator;
private readonly ICommandSchemaResolver _commandSchemaResolver;
private readonly ICommandFactory _commandFactory;
private readonly ICommandInitializer _commandInitializer;
private readonly IHelpTextRenderer _helpTextRenderer;
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CliApplication"/>. /// Initializes an instance of <see cref="CliApplication"/>.
/// </summary> /// </summary>
public CliApplication(ApplicationMetadata metadata, ApplicationConfiguration configuration, public CliApplication(
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver, ApplicationMetadata metadata, ApplicationConfiguration configuration,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer) IConsole console, ITypeActivator typeActivator)
{ {
_metadata = metadata.GuardNotNull(nameof(metadata)); _metadata = metadata;
_configuration = configuration.GuardNotNull(nameof(configuration)); _configuration = configuration;
_console = console;
_console = console.GuardNotNull(nameof(console)); _typeActivator = typeActivator;
_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));
} }
/// <inheritdoc /> private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{ {
commandLineArguments.GuardNotNull(nameof(commandLineArguments)); var isDebugMode = _configuration.IsDebugModeAllowed && commandLineInput.IsDebugDirectiveSpecified;
if (!isDebugMode)
return null;
_console.WithForegroundColor(ConsoleColor.Green, () =>
_console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
while (!Debugger.IsAttached)
await Task.Delay(100);
return null;
}
private int? HandlePreviewDirective(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
{
var isPreviewMode = _configuration.IsPreviewModeAllowed && commandLineInput.IsPreviewDirectiveSpecified;
if (!isPreviewMode)
return null;
var commandSchema = applicationSchema.TryFindCommand(commandLineInput, out var argumentOffset);
_console.Output.WriteLine("Parser preview:");
// Command name
if (commandSchema != null && argumentOffset > 0)
{
_console.WithForegroundColor(ConsoleColor.Cyan, () =>
_console.Output.Write(commandSchema.Name));
_console.Output.Write(' ');
}
// Parameters
foreach (var parameter in commandLineInput.Arguments.Skip(argumentOffset))
{
_console.Output.Write('<');
_console.WithForegroundColor(ConsoleColor.White, () =>
_console.Output.Write(parameter));
_console.Output.Write('>');
_console.Output.Write(' ');
}
// 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(CommandLineInput commandLineInput)
{
// 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;
_console.Output.WriteLine(_metadata.VersionText);
return 0;
}
private int? HandleHelpOption(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
{
// 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 (!shouldRenderHelp)
return null;
// Get the command schema that matches the input or use a dummy default command as a fallback
var commandSchema =
applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand;
RenderHelp(applicationSchema, commandSchema);
return 0;
}
private async ValueTask<int> HandleCommandExecutionAsync(
ApplicationSchema applicationSchema,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables)
{
await applicationSchema
.InitializeEntryPoint(commandLineInput, environmentVariables, _typeActivator)
.ExecuteAsync(_console);
return 0;
}
/// <summary>
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
/// </summary>
public async ValueTask<int> RunAsync(
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables)
{
try try
{ {
// Get schemas for all available command types var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes); var commandLineInput = CommandLineInput.Parse(commandLineArguments);
// Parse command input from arguments return
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments); await HandleDebugDirectiveAsync(commandLineInput) ??
HandlePreviewDirective(applicationSchema, commandLineInput) ??
// Find command schema matching the name specified in the input HandleVersionOption(commandLineInput) ??
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName); HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
// Handle cases where requested command is not defined
if (targetCommandSchema == null)
{
var isError = false;
// If specified a command - show error
if (commandInput.IsCommandSpecified())
{
isError = true;
_console.WithForegroundColor(ConsoleColor.Red,
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
}
// Get parent command schema
var parentCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
// Show help for parent command if it's defined
if (parentCommandSchema != null)
{
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, parentCommandSchema);
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
}
// Otherwise show help for a stub default command
else
{
var helpTextSource = new HelpTextSource(_metadata,
availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray(),
CommandSchema.StubDefaultCommand);
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
}
return isError ? -1 : 0;
}
// Show version if it was requested without specifying a command
if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
{
_console.Output.WriteLine(_metadata.VersionText);
return 0;
}
// Show help if it was requested
if (commandInput.IsHelpRequested())
{
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
return 0;
}
// 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);
return 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
// We want to catch exceptions in order to print errors and return correct exit codes. // We want to catch exceptions in order to print errors and return correct exit codes.
// Also, by doing this we get rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions. // Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
// In case we catch a CliFx-specific exception, we want to just show the error message, not the stack trace. // Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
// Stack trace isn't very useful to the user if the exception is not really coming from their code. var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
? ex.Message
: ex.ToString();
// CommandException is the same, but it also lets users specify exit code so we want to return that instead of default. _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
var message = ex is CliFxException && !ex.Message.IsNullOrWhiteSpace() ? ex.Message : ex.ToString(); return ex is CommandException commandException
var exitCode = ex is CommandException commandEx ? commandEx.ExitCode : ex.HResult; ? commandException.ExitCode
: ex.HResult;
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(message));
return exitCode;
} }
} }
/// <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,157 +3,171 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using CliFx.Internal; using CliFx.Domain;
using CliFx.Models;
using CliFx.Services;
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplicationBuilder"/>. /// Builds an instance of <see cref="CliApplication"/>.
/// </summary> /// </summary>
public partial class CliApplicationBuilder : ICliApplicationBuilder public partial class CliApplicationBuilder
{ {
private readonly HashSet<Type> _commandTypes = new HashSet<Type>(); private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
private string _title; private bool _isDebugModeAllowed = true;
private string _executableName; private bool _isPreviewModeAllowed = true;
private string _versionText; private string? _title;
private string _description; private string? _executableName;
private IConsole _console; private string? _versionText;
private ICommandFactory _commandFactory; private string? _description;
private IConsole? _console;
private ITypeActivator? _typeActivator;
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AddCommand(Type commandType) /// Adds a command of specified type to the application.
/// </summary>
public CliApplicationBuilder AddCommand(Type commandType)
{ {
commandType.GuardNotNull(nameof(commandType));
_commandTypes.Add(commandType); _commandTypes.Add(commandType);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) /// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
{ {
commandAssembly.GuardNotNull(nameof(commandAssembly));
var commandTypes = commandAssembly.ExportedTypes
.Where(t => t.Implements(typeof(ICommand)))
.Where(t => !t.IsAbstract && !t.IsInterface);
foreach (var commandType in commandTypes) foreach (var commandType in commandTypes)
AddCommand(commandType); AddCommand(commandType);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseTitle(string title) /// Adds commands from the specified assembly to the application.
/// Only the public types are added.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{ {
_title = title.GuardNotNull(nameof(title)); foreach (var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType))
AddCommand(commandType);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseExecutableName(string executableName) /// Adds commands from the specified assemblies to the application.
/// Only the public types are added.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
{ {
_executableName = executableName.GuardNotNull(nameof(executableName)); foreach (var commandAssembly in commandAssemblies)
AddCommandsFrom(commandAssembly);
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseVersionText(string versionText) /// 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)
{ {
_versionText = versionText.GuardNotNull(nameof(versionText)); _isDebugModeAllowed = isAllowed;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseDescription(string description) /// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
/// </summary>
public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
{ {
_description = description; // can be null _isPreviewModeAllowed = isAllowed;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseConsole(IConsole console) /// Sets application title, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseTitle(string title)
{ {
_console = console.GuardNotNull(nameof(console)); _title = title;
return this; return this;
} }
/// <inheritdoc /> /// <summary>
public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory) /// Sets application executable name, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseExecutableName(string executableName)
{ {
_commandFactory = factory.GuardNotNull(nameof(factory)); _executableName = executableName;
return this; return this;
} }
private void SetFallbackValues() /// <summary>
/// Sets application version text, which appears in the help text and when the user requests version information.
/// </summary>
public CliApplicationBuilder UseVersionText(string versionText)
{ {
if (_title.IsNullOrWhiteSpace()) _versionText = versionText;
{ return this;
// Entry assembly is null in tests
UseTitle(EntryAssembly?.GetName().Name ?? "App");
}
if (_executableName.IsNullOrWhiteSpace())
{
// Entry assembly is null in tests
var entryAssemblyLocation = EntryAssembly?.Location;
// Set different executable name depending on location
if (!entryAssemblyLocation.IsNullOrWhiteSpace())
{
// Prepend 'dotnet' to assembly file name if the entry assembly is a dll file (extension needs to be kept)
if (string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase))
{
UseExecutableName("dotnet " + Path.GetFileName(entryAssemblyLocation));
}
// Otherwise just use assembly file name without extension
else
{
UseExecutableName(Path.GetFileNameWithoutExtension(entryAssemblyLocation));
}
}
// If location is null then just use a stub
else
{
UseExecutableName("app");
}
}
if (_versionText.IsNullOrWhiteSpace())
{
// Entry assembly is null in tests
UseVersionText(EntryAssembly?.GetName().Version.ToString() ?? "1.0");
}
if (_console == null)
{
UseConsole(new SystemConsole());
}
if (_commandFactory == null)
{
UseCommandFactory(new CommandFactory());
}
} }
/// <inheritdoc /> /// <summary>
public ICliApplication Build() /// Sets application description, which appears in the help text.
/// </summary>
public CliApplicationBuilder UseDescription(string? description)
{ {
// Use defaults for required parameters that were not configured _description = description;
SetFallbackValues(); return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="IConsole"/>.
/// </summary>
public CliApplicationBuilder UseConsole(IConsole console)
{
_console = console;
return this;
}
/// <summary>
/// Configures the application to use the specified implementation of <see cref="ITypeActivator"/>.
/// </summary>
public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator)
{
_typeActivator = typeActivator;
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));
/// <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()
{
_title ??= GetDefaultTitle() ?? "App";
_executableName ??= GetDefaultExecutableName() ?? "app";
_versionText ??= GetDefaultVersionText() ?? "v1.0";
_console ??= new SystemConsole();
_typeActivator ??= new DefaultTypeActivator();
// Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description); var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
var configuration = new ApplicationConfiguration(_commandTypes.ToArray()); var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration, return new CliApplication(metadata, configuration, _console, _typeActivator);
_console, new CommandInputParser(), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(), new HelpTextRenderer());
} }
} }
@@ -161,6 +175,28 @@ namespace CliFx
{ {
private static readonly Lazy<Assembly> LazyEntryAssembly = new Lazy<Assembly>(Assembly.GetEntryAssembly); private static readonly Lazy<Assembly> LazyEntryAssembly = new Lazy<Assembly>(Assembly.GetEntryAssembly);
// Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value; private static Assembly EntryAssembly => LazyEntryAssembly.Value;
private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
private static string? GetDefaultExecutableName()
{
var entryAssemblyLocation = EntryAssembly?.Location;
// If it's a .dll assembly, prepend 'dotnet' and keep the file extension
if (string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase))
{
return "dotnet " + Path.GetFileName(entryAssemblyLocation);
}
// Otherwise just use assembly file name without extension
return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
}
private static string? GetDefaultVersionText() =>
EntryAssembly != null
? $"v{EntryAssembly.GetName().Version}"
: null;
} }
} }

View File

@@ -1,23 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks> <TargetFrameworks>netstandard2.1;netstandard2.0;net45</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.0.3</Version>
<Company>Tyrrrz</Company>
<Authors>$(Company)</Authors> <Authors>$(Company)</Authors>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<Description>Declarative framework for CLI applications</Description> <Description>Declarative framework for CLI applications</Description>
<PackageTags>command line executable interface framework parser arguments net core</PackageTags> <PackageTags>command line executable interface framework parser arguments net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl> <PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
<PackageIconUrl>https://raw.githubusercontent.com/Tyrrrz/CliFx/master/favicon.png</PackageIconUrl> <PackageIcon>favicon.png</PackageIcon>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile> <PublishRepositoryUrl>True</PublishRepositoryUrl>
<EmbedUntrackedSources>True</EmbedUntrackedSources>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup>
<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> </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());
}
}

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