mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e04f79469 | ||
|
|
cd55898011 | ||
|
|
272c079767 | ||
|
|
256b693466 | ||
|
|
89cc3c8785 | ||
|
|
43e3042bac | ||
|
|
c906833ac7 | ||
|
|
dd882a6372 | ||
|
|
3017c3d6c3 | ||
|
|
4b98dbf51f | ||
|
|
e652f9bda4 | ||
|
|
21c550d99c | ||
|
|
23d29a8309 | ||
|
|
70796c1254 | ||
|
|
1b62b2ded2 | ||
|
|
a9f4958c92 | ||
|
|
66f9b1a256 | ||
|
|
de8513c6fa | ||
|
|
105dc88ccd | ||
|
|
b736eeaf7d | ||
|
|
04415cbfc1 | ||
|
|
45c2b9c4e0 | ||
|
|
78ffaeb4b2 | ||
|
|
08e2874eb4 | ||
|
|
6648ae22eb | ||
|
|
bd6b1a1134 | ||
|
|
d5b95bf1f1 | ||
|
|
f5c34ca454 | ||
|
|
63f583b02a | ||
|
|
fa82f892e4 | ||
|
|
5a696c181b | ||
|
|
7d7edaf30f | ||
|
|
172ec1f15e | ||
|
|
e5bbda5892 | ||
|
|
fc1568ce20 | ||
|
|
efd8bbe89f | ||
|
|
2d8b0b4c88 | ||
|
|
87688ec29e | ||
|
|
ddc1ae8537 | ||
|
|
5104a2ebf9 | ||
|
|
b6ea1c3df0 | ||
|
|
cf521a9fb3 | ||
|
|
b5fa60a26b | ||
|
|
500378070d | ||
|
|
24c892b1ab | ||
|
|
f1554fd08a | ||
|
|
5a08b8c19b | ||
|
|
7dfbb40860 | ||
|
|
743241cb3b | ||
|
|
384482a47c | ||
|
|
86fdf72d9c | ||
|
|
dc067ba224 | ||
|
|
a322632e46 | ||
|
|
f09caa876f | ||
|
|
018320582b | ||
|
|
18429827df | ||
|
|
b050ca4d67 | ||
|
|
f8cd2a56b2 | ||
|
|
6a06cdc422 | ||
|
|
b0d9626e74 | ||
|
|
f47cd3774e | ||
|
|
ed72571ddc | ||
|
|
e7e47b1c9d | ||
|
|
50df046754 | ||
|
|
041a995c62 | ||
|
|
5174d5354b | ||
|
|
9856e784f5 | ||
|
|
16676cff8c | ||
|
|
d9c27dc82a | ||
|
|
5bb175fd4b | ||
|
|
d72391df1f | ||
|
|
c1ee1a968a | ||
|
|
4e9effe481 | ||
|
|
5ac9b33056 | ||
|
|
a64a8fc651 | ||
|
|
24eef8957d | ||
|
|
dd2789790e | ||
|
|
d2599af90b | ||
|
|
2bdb2bddc8 | ||
|
|
77c7faa759 | ||
|
|
4ba9413012 | ||
|
|
3611aa51e6 | ||
|
|
74ee927498 | ||
|
|
79cf994386 | ||
|
|
7a5a32d27b | ||
|
|
1543076bf4 | ||
|
|
63d798977d | ||
|
|
e0211fc141 | ||
|
|
fd6ed3ca72 | ||
|
|
3a9ac3d36c |
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github: Tyrrrz
|
||||||
|
patreon: Tyrrrz
|
||||||
|
open_collective: Tyrrrz
|
||||||
|
custom: ['buymeacoffee.com/Tyrrrz']
|
||||||
34
CliFx.Benchmarks/Benchmark.cs
Normal file
34
CliFx.Benchmarks/Benchmark.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using CliFx.Benchmarks.Commands;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
[CoreJob]
|
||||||
|
[RankColumn]
|
||||||
|
public class Benchmark
|
||||||
|
{
|
||||||
|
private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" };
|
||||||
|
|
||||||
|
[Benchmark(Description = "CliFx", Baseline = true)]
|
||||||
|
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
|
||||||
|
|
||||||
|
[Benchmark(Description = "System.CommandLine")]
|
||||||
|
public Task<int> ExecuteWithSystemCommandLine() => new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
||||||
|
|
||||||
|
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
||||||
|
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
||||||
|
|
||||||
|
// Skipped because this benchmark freezes after a couple of iterations
|
||||||
|
// Probably wasn't designed to run multiple times in single process execution
|
||||||
|
//[Benchmark(Description = "CommandLineParser")]
|
||||||
|
public void ExecuteWithCommandLineParser()
|
||||||
|
{
|
||||||
|
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand));
|
||||||
|
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Description = "PowerArgs")]
|
||||||
|
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
CliFx.Benchmarks/CliFx.Benchmarks.csproj
Normal file
21
CliFx.Benchmarks/CliFx.Benchmarks.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.6.0" />
|
||||||
|
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
|
||||||
|
<PackageReference Include="PowerArgs" Version="3.6.0" />
|
||||||
|
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
21
CliFx.Benchmarks/Commands/CliFxCommand.cs
Normal file
21
CliFx.Benchmarks/Commands/CliFxCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks.Commands
|
||||||
|
{
|
||||||
|
[Command]
|
||||||
|
public class CliFxCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("str", 's')]
|
||||||
|
public string StrOption { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("int", 'i')]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("bool", 'b')]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
CliFx.Benchmarks/Commands/CommandLineParserCommand.cs
Normal file
20
CliFx.Benchmarks/Commands/CommandLineParserCommand.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks.Commands
|
||||||
|
{
|
||||||
|
public class CommandLineParserCommand
|
||||||
|
{
|
||||||
|
[Option('s', "str")]
|
||||||
|
public string StrOption { get; set; }
|
||||||
|
|
||||||
|
[Option('i', "int")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[Option('b', "bool")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Benchmarks/Commands/McMasterCommand.cs
Normal file
18
CliFx.Benchmarks/Commands/McMasterCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks.Commands
|
||||||
|
{
|
||||||
|
public class McMasterCommand
|
||||||
|
{
|
||||||
|
[Option("--str|-s")]
|
||||||
|
public string StrOption { get; set; }
|
||||||
|
|
||||||
|
[Option("--int|-i")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[Option("--bool|-b")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public int OnExecute() => 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
CliFx.Benchmarks/Commands/PowerArgsCommand.cs
Normal file
20
CliFx.Benchmarks/Commands/PowerArgsCommand.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using PowerArgs;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks.Commands
|
||||||
|
{
|
||||||
|
public class PowerArgsCommand
|
||||||
|
{
|
||||||
|
[ArgShortcut("--str"), ArgShortcut("-s")]
|
||||||
|
public string StrOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--int"), ArgShortcut("-i")]
|
||||||
|
public int IntOption { get; set; }
|
||||||
|
|
||||||
|
[ArgShortcut("--bool"), ArgShortcut("-b")]
|
||||||
|
public bool BoolOption { get; set; }
|
||||||
|
|
||||||
|
public void Main()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
CliFx.Benchmarks/Commands/SystemCommandLineCommand.cs
Normal file
34
CliFx.Benchmarks/Commands/SystemCommandLineCommand.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.CommandLine.Invocation;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks.Commands
|
||||||
|
{
|
||||||
|
public class SystemCommandLineCommand
|
||||||
|
{
|
||||||
|
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
||||||
|
|
||||||
|
public Task<int> ExecuteAsync(string[] args)
|
||||||
|
{
|
||||||
|
var command = new RootCommand
|
||||||
|
{
|
||||||
|
new Option(new[] {"--str", "-s"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<string>()
|
||||||
|
},
|
||||||
|
new Option(new[] {"--int", "-i"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<int>()
|
||||||
|
},
|
||||||
|
new Option(new[] {"--bool", "-b"})
|
||||||
|
{
|
||||||
|
Argument = new Argument<bool>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler)));
|
||||||
|
|
||||||
|
return command.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
CliFx.Benchmarks/Program.cs
Normal file
12
CliFx.Benchmarks/Program.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
namespace CliFx.Benchmarks
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static void Main() =>
|
||||||
|
BenchmarkRunner.Run(typeof(Program).Assembly, DefaultConfig.Instance
|
||||||
|
.With(ConfigOptions.DisableOptimizationsValidator));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Demo/CliFx.Demo.csproj
Normal file
18
CliFx.Demo/CliFx.Demo.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
75
CliFx.Demo/Commands/BookAddCommand.cs
Normal file
75
CliFx.Demo/Commands/BookAddCommand.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Demo.Internal;
|
||||||
|
using CliFx.Demo.Models;
|
||||||
|
using CliFx.Demo.Services;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Commands
|
||||||
|
{
|
||||||
|
[Command("book add", Description = "Add a book to the library.")]
|
||||||
|
public partial class BookAddCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly LibraryService _libraryService;
|
||||||
|
|
||||||
|
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
||||||
|
public string Author { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("published", 'p', Description = "Book publish date.")]
|
||||||
|
public DateTimeOffset Published { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
||||||
|
public Isbn Isbn { get; set; }
|
||||||
|
|
||||||
|
public BookAddCommand(LibraryService libraryService)
|
||||||
|
{
|
||||||
|
_libraryService = libraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set
|
||||||
|
if (Published == default)
|
||||||
|
Published = CreateRandomDate();
|
||||||
|
if (Isbn == default)
|
||||||
|
Isbn = CreateRandomIsbn();
|
||||||
|
|
||||||
|
if (_libraryService.GetBook(Title) != null)
|
||||||
|
throw new CommandException("Book already exists.", 1);
|
||||||
|
|
||||||
|
var book = new Book(Title, Author, Published, Isbn);
|
||||||
|
_libraryService.AddBook(book);
|
||||||
|
|
||||||
|
console.Output.WriteLine("Book added.");
|
||||||
|
console.RenderBook(book);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class BookAddCommand
|
||||||
|
{
|
||||||
|
private static readonly Random Random = new Random();
|
||||||
|
|
||||||
|
private static DateTimeOffset CreateRandomDate() => new DateTimeOffset(
|
||||||
|
Random.Next(1800, 2020),
|
||||||
|
Random.Next(1, 12),
|
||||||
|
Random.Next(1, 28),
|
||||||
|
Random.Next(1, 23),
|
||||||
|
Random.Next(1, 59),
|
||||||
|
Random.Next(1, 59),
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
public static Isbn CreateRandomIsbn() => new Isbn(
|
||||||
|
Random.Next(0, 999),
|
||||||
|
Random.Next(0, 99),
|
||||||
|
Random.Next(0, 99999),
|
||||||
|
Random.Next(0, 99),
|
||||||
|
Random.Next(0, 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
CliFx.Demo/Commands/BookCommand.cs
Normal file
35
CliFx.Demo/Commands/BookCommand.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Demo.Internal;
|
||||||
|
using CliFx.Demo.Services;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Commands
|
||||||
|
{
|
||||||
|
[Command("book", Description = "View, list, add or remove books.")]
|
||||||
|
public class BookCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly LibraryService _libraryService;
|
||||||
|
|
||||||
|
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public BookCommand(LibraryService libraryService)
|
||||||
|
{
|
||||||
|
_libraryService = libraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
var book = _libraryService.GetBook(Title);
|
||||||
|
|
||||||
|
if (book == null)
|
||||||
|
throw new CommandException("Book not found.", 1);
|
||||||
|
|
||||||
|
console.RenderBook(book);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
CliFx.Demo/Commands/BookListCommand.cs
Normal file
38
CliFx.Demo/Commands/BookListCommand.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Demo.Internal;
|
||||||
|
using CliFx.Demo.Services;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Commands
|
||||||
|
{
|
||||||
|
[Command("book list", Description = "List all books in the library.")]
|
||||||
|
public class BookListCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly LibraryService _libraryService;
|
||||||
|
|
||||||
|
public BookListCommand(LibraryService libraryService)
|
||||||
|
{
|
||||||
|
_libraryService = libraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
var library = _libraryService.GetLibrary();
|
||||||
|
|
||||||
|
var isFirst = true;
|
||||||
|
foreach (var book in library.Books)
|
||||||
|
{
|
||||||
|
// Margin
|
||||||
|
if (!isFirst)
|
||||||
|
console.Output.WriteLine();
|
||||||
|
isFirst = false;
|
||||||
|
|
||||||
|
// Render book
|
||||||
|
console.RenderBook(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
CliFx.Demo/Commands/BookRemoveCommand.cs
Normal file
36
CliFx.Demo/Commands/BookRemoveCommand.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Demo.Services;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Commands
|
||||||
|
{
|
||||||
|
[Command("book remove", Description = "Remove a book from the library.")]
|
||||||
|
public class BookRemoveCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly LibraryService _libraryService;
|
||||||
|
|
||||||
|
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public BookRemoveCommand(LibraryService libraryService)
|
||||||
|
{
|
||||||
|
_libraryService = libraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
var book = _libraryService.GetBook(Title);
|
||||||
|
|
||||||
|
if (book == null)
|
||||||
|
throw new CommandException("Book not found.", 1);
|
||||||
|
|
||||||
|
_libraryService.RemoveBook(book);
|
||||||
|
|
||||||
|
console.Output.WriteLine($"Book {Title} removed.");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
CliFx.Demo/Internal/Extensions.cs
Normal file
30
CliFx.Demo/Internal/Extensions.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Demo.Models;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Internal
|
||||||
|
{
|
||||||
|
internal static class Extensions
|
||||||
|
{
|
||||||
|
public static void RenderBook(this IConsole console, Book book)
|
||||||
|
{
|
||||||
|
// Title
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title));
|
||||||
|
|
||||||
|
// Author
|
||||||
|
console.Output.Write(" ");
|
||||||
|
console.Output.Write("Author: ");
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author));
|
||||||
|
|
||||||
|
// Published
|
||||||
|
console.Output.Write(" ");
|
||||||
|
console.Output.Write("Published: ");
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}"));
|
||||||
|
|
||||||
|
// ISBN
|
||||||
|
console.Output.Write(" ");
|
||||||
|
console.Output.Write("ISBN: ");
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
CliFx.Demo/Models/Book.cs
Normal file
23
CliFx.Demo/Models/Book.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Models
|
||||||
|
{
|
||||||
|
public class Book
|
||||||
|
{
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
public string Author { get; }
|
||||||
|
|
||||||
|
public DateTimeOffset Published { get; }
|
||||||
|
|
||||||
|
public Isbn Isbn { get; }
|
||||||
|
|
||||||
|
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
Author = author;
|
||||||
|
Published = published;
|
||||||
|
Isbn = isbn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
CliFx.Demo/Models/Extensions.cs
Normal file
22
CliFx.Demo/Models/Extensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Models
|
||||||
|
{
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static Library WithBook(this Library library, Book book)
|
||||||
|
{
|
||||||
|
var books = library.Books.ToList();
|
||||||
|
books.Add(book);
|
||||||
|
|
||||||
|
return new Library(books);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Library WithoutBook(this Library library, Book book)
|
||||||
|
{
|
||||||
|
var books = library.Books.Where(b => b != book).ToArray();
|
||||||
|
|
||||||
|
return new Library(books);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
CliFx.Demo/Models/Isbn.cs
Normal file
44
CliFx.Demo/Models/Isbn.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Models
|
||||||
|
{
|
||||||
|
public partial class Isbn
|
||||||
|
{
|
||||||
|
public int EanPrefix { get; }
|
||||||
|
|
||||||
|
public int RegistrationGroup { get; }
|
||||||
|
|
||||||
|
public int Registrant { get; }
|
||||||
|
|
||||||
|
public int Publication { get; }
|
||||||
|
|
||||||
|
public int CheckDigit { get; }
|
||||||
|
|
||||||
|
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
|
||||||
|
{
|
||||||
|
EanPrefix = eanPrefix;
|
||||||
|
RegistrationGroup = registrationGroup;
|
||||||
|
Registrant = registrant;
|
||||||
|
Publication = publication;
|
||||||
|
CheckDigit = checkDigit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class Isbn
|
||||||
|
{
|
||||||
|
public static Isbn Parse(string value)
|
||||||
|
{
|
||||||
|
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
return new Isbn(
|
||||||
|
int.Parse(components[0], CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(components[1], CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(components[2], CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(components[3], CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(components[4], CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
CliFx.Demo/Models/Library.cs
Normal file
20
CliFx.Demo/Models/Library.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Models
|
||||||
|
{
|
||||||
|
public partial class Library
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Book> Books { get; }
|
||||||
|
|
||||||
|
public Library(IReadOnlyList<Book> books)
|
||||||
|
{
|
||||||
|
Books = books;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class Library
|
||||||
|
{
|
||||||
|
public static Library Empty { get; } = new Library(Array.Empty<Book>());
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CliFx.Demo/Program.cs
Normal file
33
CliFx.Demo/Program.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Demo.Commands;
|
||||||
|
using CliFx.Demo.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace CliFx.Demo
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// Register services
|
||||||
|
services.AddSingleton<LibraryService>();
|
||||||
|
|
||||||
|
// Register commands
|
||||||
|
services.AddTransient<BookCommand>();
|
||||||
|
services.AddTransient<BookAddCommand>();
|
||||||
|
services.AddTransient<BookRemoveCommand>();
|
||||||
|
services.AddTransient<BookListCommand>();
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
return new CliApplicationBuilder()
|
||||||
|
.AddCommandsFromThisAssembly()
|
||||||
|
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
|
||||||
|
.Build()
|
||||||
|
.RunAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
CliFx.Demo/Readme.md
Normal file
7
CliFx.Demo/Readme.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# CliFx Demo Project
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You can get a list of available commands by running `CliFx.Demo --help`.
|
||||||
42
CliFx.Demo/Services/LibraryService.cs
Normal file
42
CliFx.Demo/Services/LibraryService.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Demo.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CliFx.Demo.Services
|
||||||
|
{
|
||||||
|
public class LibraryService
|
||||||
|
{
|
||||||
|
private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
|
||||||
|
|
||||||
|
private void StoreLibrary(Library library)
|
||||||
|
{
|
||||||
|
var data = JsonConvert.SerializeObject(library);
|
||||||
|
File.WriteAllText(StorageFilePath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Library GetLibrary()
|
||||||
|
{
|
||||||
|
if (!File.Exists(StorageFilePath))
|
||||||
|
return Library.Empty;
|
||||||
|
|
||||||
|
var data = File.ReadAllText(StorageFilePath);
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<Library>(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||||
|
|
||||||
|
public void AddBook(Book book)
|
||||||
|
{
|
||||||
|
var updatedLibrary = GetLibrary().WithBook(book);
|
||||||
|
StoreLibrary(updatedLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveBook(Book book)
|
||||||
|
{
|
||||||
|
var updatedLibrary = GetLibrary().WithoutBook(book);
|
||||||
|
StoreLibrary(updatedLibrary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net46</TargetFramework>
|
||||||
|
<Version>1.2.3.4</Version>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
|
||||||
{
|
|
||||||
[Command("add")]
|
|
||||||
public class AddCommand : Command
|
|
||||||
{
|
|
||||||
[CommandOption("a", IsRequired = true, Description = "Left operand.")]
|
|
||||||
public double A { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("b", IsRequired = true, Description = "Right operand.")]
|
|
||||||
public double B { get; set; }
|
|
||||||
|
|
||||||
public override ExitCode Execute()
|
|
||||||
{
|
|
||||||
var result = A + B;
|
|
||||||
Console.WriteLine(result.ToString(CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
return ExitCode.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
|
||||||
{
|
|
||||||
[DefaultCommand]
|
|
||||||
public class DefaultCommand : Command
|
|
||||||
{
|
|
||||||
[CommandOption("target", ShortName = 't', Description = "Greeting target.")]
|
|
||||||
public string Target { get; set; } = "world";
|
|
||||||
|
|
||||||
[CommandOption("enthusiastic", ShortName = 'e', Description = "Whether the greeting should be enthusiastic.")]
|
|
||||||
public bool IsEnthusiastic { get; set; }
|
|
||||||
|
|
||||||
public override ExitCode Execute()
|
|
||||||
{
|
|
||||||
var buffer = new StringBuilder();
|
|
||||||
|
|
||||||
buffer.Append("Hello ").Append(Target);
|
|
||||||
|
|
||||||
if (IsEnthusiastic)
|
|
||||||
buffer.Append("!!!");
|
|
||||||
|
|
||||||
Console.WriteLine(buffer.ToString());
|
|
||||||
|
|
||||||
return ExitCode.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
CliFx.Tests.Dummy/Commands/GreeterCommand.cs
Normal file
31
CliFx.Tests.Dummy/Commands/GreeterCommand.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Models;
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy.Commands
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
{
|
{
|
||||||
[Command("log")]
|
[Command("log", Description = "Calculate the logarithm of a value.")]
|
||||||
public class LogCommand : Command
|
public class LogCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("value", IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
||||||
public double Value { get; set; }
|
public double Value { get; set; }
|
||||||
|
|
||||||
[CommandOption("base", Description = "Logarithm base.")]
|
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||||
public double Base { get; set; } = 10;
|
public double Base { get; set; } = 10;
|
||||||
|
|
||||||
public override ExitCode Execute()
|
public Task ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var result = Math.Log(Value, Base);
|
var result = Math.Log(Value, Base);
|
||||||
Console.WriteLine(result.ToString(CultureInfo.InvariantCulture));
|
console.Output.WriteLine(result);
|
||||||
|
|
||||||
return ExitCode.Success;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
23
CliFx.Tests.Dummy/Commands/SumCommand.cs
Normal file
23
CliFx.Tests.Dummy/Commands/SumCommand.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CliFx.Tests.Dummy
|
namespace CliFx.Tests.Dummy
|
||||||
{
|
{
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
public static Task<int> Main(string[] args) => new CliApplication().RunAsync(args);
|
public static Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
// 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()
|
||||||
|
.UseDescription("Dummy program used for E2E tests.")
|
||||||
|
.Build()
|
||||||
|
.RunAsync(args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
53
CliFx.Tests/CliApplicationTests.Commands.cs
Normal file
53
CliFx.Tests/CliApplicationTests.Commands.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,174 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
using CliFx.Tests.TestObjects;
|
using FluentAssertions;
|
||||||
using Moq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace CliFx.Tests
|
namespace CliFx.Tests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CliApplicationTests
|
public partial class CliApplicationTests
|
||||||
{
|
{
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new string[0],
|
||||||
|
"DefaultCommand executed."
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(NamedCommand)},
|
||||||
|
new[] {"cmd"},
|
||||||
|
"NamedCommand executed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_HelpAndVersion_RunAsync()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new string[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new[] {"-h"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new[] {"--help"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new[] {"--version"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(NamedCommand)},
|
||||||
|
new string[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(NamedCommand)},
|
||||||
|
new[] {"cmd", "-h"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand1)},
|
||||||
|
new[] {"faulty1", "-h"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand2)},
|
||||||
|
new[] {"faulty2", "-h"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand3)},
|
||||||
|
new[] {"faulty3", "-h"}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new Type[0],
|
||||||
|
new string[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(DefaultCommand)},
|
||||||
|
new[] {"non-existing"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand1)},
|
||||||
|
new[] {"faulty1"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand2)},
|
||||||
|
new[] {"faulty2"}
|
||||||
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new[] {typeof(FaultyCommand3)},
|
||||||
|
new[] {"faulty3"}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task RunAsync_Test()
|
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||||
|
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, string expectedStdOut)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var command = new TestCommand();
|
using (var stdout = new StringWriter())
|
||||||
var expectedExitCode = await command.ExecuteAsync();
|
{
|
||||||
|
var console = new VirtualConsole(stdout);
|
||||||
|
|
||||||
var commandResolverMock = new Mock<ICommandResolver>();
|
var application = new CliApplicationBuilder()
|
||||||
commandResolverMock.Setup(m => m.ResolveCommand(It.IsAny<IReadOnlyList<string>>())).Returns(command);
|
.AddCommands(commandTypes)
|
||||||
var commandResolver = commandResolverMock.Object;
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
var application = new CliApplication(commandResolver);
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(commandLineArguments);
|
||||||
|
|
||||||
// Act
|
// Assert
|
||||||
var exitCodeValue = await application.RunAsync();
|
exitCode.Should().Be(0);
|
||||||
|
stdout.ToString().Trim().Should().Be(expectedStdOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assert
|
[Test]
|
||||||
Assert.That(exitCodeValue, Is.EqualTo(expectedExitCode.Value));
|
[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))]
|
||||||
|
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using (var stderr = new StringWriter())
|
||||||
|
{
|
||||||
|
var console = new VirtualConsole(TextWriter.Null, stderr);
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommands(commandTypes)
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(commandLineArguments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stderr.ToString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net46</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<CollectCoverage>true</CollectCoverage>
|
||||||
|
<CoverletOutputFormat>opencover</CoverletOutputFormat>
|
||||||
|
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
<PackageReference Include="FluentAssertions" Version="5.8.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="Moq" Version="4.11.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
|
||||||
<PackageReference Include="CliWrap" Version="2.3.0" />
|
<PackageReference Include="CliWrap" Version="2.3.1" />
|
||||||
|
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
15
CliFx.Tests/CommandFactoryTests.Commands.cs
Normal file
15
CliFx.Tests/CommandFactoryTests.Commands.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
CliFx.Tests/CommandFactoryTests.cs
Normal file
36
CliFx.Tests/CommandFactoryTests.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
CliFx.Tests/CommandInitializerTests.Commands.cs
Normal file
21
CliFx.Tests/CommandInitializerTests.Commands.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
CliFx.Tests/CommandInitializerTests.cs
Normal file
96
CliFx.Tests/CommandInitializerTests.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
CliFx.Tests/CommandInputParserTests.cs
Normal file
184
CliFx.Tests/CommandInputParserTests.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestObjects;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandOptionConverterTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetData_ConvertOption()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData("value", typeof(string), "value")
|
|
||||||
.SetName("To string");
|
|
||||||
|
|
||||||
yield return new TestCaseData("value", typeof(object), "value")
|
|
||||||
.SetName("To object");
|
|
||||||
|
|
||||||
yield return new TestCaseData("true", typeof(bool), true)
|
|
||||||
.SetName("To bool (true)");
|
|
||||||
|
|
||||||
yield return new TestCaseData("false", typeof(bool), false)
|
|
||||||
.SetName("To bool (false)");
|
|
||||||
|
|
||||||
yield return new TestCaseData(null, typeof(bool), true)
|
|
||||||
.SetName("To bool (switch)");
|
|
||||||
|
|
||||||
yield return new TestCaseData("123", typeof(int), 123)
|
|
||||||
.SetName("To int");
|
|
||||||
|
|
||||||
yield return new TestCaseData("123.45", typeof(double), 123.45)
|
|
||||||
.SetName("To double");
|
|
||||||
|
|
||||||
yield return new TestCaseData("28 Apr 1995", typeof(DateTime), new DateTime(1995, 04, 28))
|
|
||||||
.SetName("To DateTime");
|
|
||||||
|
|
||||||
yield return new TestCaseData("28 Apr 1995", typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28)))
|
|
||||||
.SetName("To DateTimeOffset");
|
|
||||||
|
|
||||||
yield return new TestCaseData("00:14:59", typeof(TimeSpan), new TimeSpan(00, 14, 59))
|
|
||||||
.SetName("To TimeSpan");
|
|
||||||
|
|
||||||
yield return new TestCaseData("value2", typeof(TestEnum), TestEnum.Value2)
|
|
||||||
.SetName("To enum");
|
|
||||||
|
|
||||||
yield return new TestCaseData("666", typeof(int?), 666)
|
|
||||||
.SetName("To int? (with value)");
|
|
||||||
|
|
||||||
yield return new TestCaseData(null, typeof(int?), null)
|
|
||||||
.SetName("To int? (no value)");
|
|
||||||
|
|
||||||
yield return new TestCaseData("value3", typeof(TestEnum?), TestEnum.Value3)
|
|
||||||
.SetName("To enum? (with value)");
|
|
||||||
|
|
||||||
yield return new TestCaseData(null, typeof(TestEnum?), null)
|
|
||||||
.SetName("To enum? (no value)");
|
|
||||||
|
|
||||||
yield return new TestCaseData("01:00:00", typeof(TimeSpan?), new TimeSpan(01, 00, 00))
|
|
||||||
.SetName("To TimeSpan? (with value)");
|
|
||||||
|
|
||||||
yield return new TestCaseData(null, typeof(TimeSpan?), null)
|
|
||||||
.SetName("To TimeSpan? (no value)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetData_ConvertOption))]
|
|
||||||
public void ConvertOption_Test(string value, Type targetType, object expectedConvertedValue)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var converter = new CommandOptionConverter();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var convertedValue = converter.ConvertOption(value, targetType);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue));
|
|
||||||
|
|
||||||
if (convertedValue != null)
|
|
||||||
Assert.That(convertedValue, Is.AssignableTo(targetType));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
CliFx.Tests/CommandOptionInputConverterTests.Types.cs
Normal file
63
CliFx.Tests/CommandOptionInputConverterTests.Types.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
305
CliFx.Tests/CommandOptionInputConverterTests.cs
Normal file
305
CliFx.Tests/CommandOptionInputConverterTests.cs
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandOptionParserTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetData_ParseOptions()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new string[0],
|
|
||||||
CommandOptionSet.Empty
|
|
||||||
).SetName("No arguments");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--argument", "value"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"argument", "value"}
|
|
||||||
})
|
|
||||||
).SetName("Single argument");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--argument1", "value1", "--argument2", "value2", "--argument3", "value3"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"argument1", "value1"},
|
|
||||||
{"argument2", "value2"},
|
|
||||||
{"argument3", "value3"}
|
|
||||||
})
|
|
||||||
).SetName("Multiple arguments");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"a", "value"}
|
|
||||||
})
|
|
||||||
).SetName("Single short argument");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value1", "-b", "value2", "-c", "value3"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"a", "value1"},
|
|
||||||
{"b", "value2"},
|
|
||||||
{"c", "value3"}
|
|
||||||
})
|
|
||||||
).SetName("Multiple short arguments");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--argument1", "value1", "-b", "value2", "--argument3", "value3"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"argument1", "value1"},
|
|
||||||
{"b", "value2"},
|
|
||||||
{"argument3", "value3"}
|
|
||||||
})
|
|
||||||
).SetName("Multiple mixed arguments");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--switch"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"switch", null}
|
|
||||||
})
|
|
||||||
).SetName("Single switch");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--switch1", "--switch2", "--switch3"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"switch1", null},
|
|
||||||
{"switch2", null},
|
|
||||||
{"switch3", null}
|
|
||||||
})
|
|
||||||
).SetName("Multiple switches");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-s"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"s", null}
|
|
||||||
})
|
|
||||||
).SetName("Single short switch");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "-b", "-c"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"a", null},
|
|
||||||
{"b", null},
|
|
||||||
{"c", null}
|
|
||||||
})
|
|
||||||
).SetName("Multiple short switches");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-abc"},
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"a", null},
|
|
||||||
{"b", null},
|
|
||||||
{"c", null}
|
|
||||||
})
|
|
||||||
).SetName("Multiple stacked short switches");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"command"},
|
|
||||||
new CommandOptionSet("command")
|
|
||||||
).SetName("No arguments (with command name)");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"command", "--argument", "value"},
|
|
||||||
new CommandOptionSet("command", new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"argument", "value"}
|
|
||||||
})
|
|
||||||
).SetName("Single argument (with command name)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetData_ParseOptions))]
|
|
||||||
public void ParseOptions_Test(IReadOnlyList<string> commandLineArguments, CommandOptionSet expectedCommandOptionSet)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var parser = new CommandOptionParser();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var optionSet = parser.ParseOptions(commandLineArguments);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName));
|
|
||||||
Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestObjects;
|
|
||||||
using Moq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandResolverTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetData_ResolveCommand()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"int", "13"}
|
|
||||||
}),
|
|
||||||
new TestCommand {IntOption = 13}
|
|
||||||
).SetName("Single option");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"int", "13"},
|
|
||||||
{"str", "hello world" }
|
|
||||||
}),
|
|
||||||
new TestCommand { IntOption = 13, StringOption = "hello world"}
|
|
||||||
).SetName("Multiple options");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"i", "13"}
|
|
||||||
}),
|
|
||||||
new TestCommand { IntOption = 13 }
|
|
||||||
).SetName("Single short option");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionSet("command", new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"int", "13"}
|
|
||||||
}),
|
|
||||||
new TestCommand { IntOption = 13 }
|
|
||||||
).SetName("Single option (with command name)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetData_ResolveCommand))]
|
|
||||||
public void ResolveCommand_Test(CommandOptionSet commandOptionSet, TestCommand expectedCommand)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var commandTypes = new[] {typeof(TestCommand)};
|
|
||||||
|
|
||||||
var typeProviderMock = new Mock<ITypeProvider>();
|
|
||||||
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
|
|
||||||
var typeProvider = typeProviderMock.Object;
|
|
||||||
|
|
||||||
var optionParserMock = new Mock<ICommandOptionParser>();
|
|
||||||
optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).Returns(commandOptionSet);
|
|
||||||
var optionParser = optionParserMock.Object;
|
|
||||||
|
|
||||||
var optionConverter = new CommandOptionConverter();
|
|
||||||
|
|
||||||
var resolver = new CommandResolver(typeProvider, optionParser, optionConverter);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var command = resolver.ResolveCommand() as TestCommand;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.That(command, Is.Not.Null);
|
|
||||||
Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption));
|
|
||||||
Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetData_ResolveCommand_IsRequired()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
CommandOptionSet.Empty
|
|
||||||
).SetName("No options");
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionSet(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{"str", "hello world"}
|
|
||||||
})
|
|
||||||
).SetName("Required option is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetData_ResolveCommand_IsRequired))]
|
|
||||||
public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var commandTypes = new[] { typeof(TestCommand) };
|
|
||||||
|
|
||||||
var typeProviderMock = new Mock<ITypeProvider>();
|
|
||||||
typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes);
|
|
||||||
var typeProvider = typeProviderMock.Object;
|
|
||||||
|
|
||||||
var optionParserMock = new Mock<ICommandOptionParser>();
|
|
||||||
optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).Returns(commandOptionSet);
|
|
||||||
var optionParser = optionParserMock.Object;
|
|
||||||
|
|
||||||
var optionConverter = new CommandOptionConverter();
|
|
||||||
|
|
||||||
var resolver = new CommandResolver(typeProvider, optionParser, optionConverter);
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
Assert.Throws<CommandResolveException>(() => resolver.ResolveCommand());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
81
CliFx.Tests/CommandSchemaResolverTests.Commands.cs
Normal file
81
CliFx.Tests/CommandSchemaResolverTests.Commands.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
CliFx.Tests/CommandSchemaResolverTests.cs
Normal file
94
CliFx.Tests/CommandSchemaResolverTests.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
CliFx.Tests/DelegateCommandFactoryTests.Commands.cs
Normal file
15
CliFx.Tests/DelegateCommandFactoryTests.Commands.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
CliFx.Tests/DelegateCommandFactoryTests.cs
Normal file
39
CliFx.Tests/DelegateCommandFactoryTests.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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 DelegateCommandFactoryTests
|
||||||
|
{
|
||||||
|
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||||
|
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)),
|
||||||
|
GetCommandSchema(typeof(TestCommand))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
|
||||||
|
public void CreateCommand_Test(Func<CommandSchema, ICommand> factoryMethod, CommandSchema commandSchema)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new DelegateCommandFactory(factoryMethod);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var command = factory.CreateCommand(commandSchema);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
command.Should().BeOfType(commandSchema.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.IO;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace CliFx.Tests
|
namespace CliFx.Tests
|
||||||
@@ -8,25 +8,66 @@ namespace CliFx.Tests
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class DummyTests
|
public class DummyTests
|
||||||
{
|
{
|
||||||
private string DummyFilePath => Path.Combine(TestContext.CurrentContext.TestDirectory, "CliFx.Tests.Dummy.exe");
|
private static string DummyFilePath => typeof(Dummy.Program).Assembly.Location;
|
||||||
|
|
||||||
|
private static string DummyVersionText => typeof(Dummy.Program).Assembly.GetName().Version.ToString();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("", "Hello world")]
|
[TestCase("", "Hello world")]
|
||||||
[TestCase("-t .NET", "Hello .NET")]
|
[TestCase("-t .NET", "Hello .NET")]
|
||||||
[TestCase("-e", "Hello world!!!")]
|
[TestCase("-e", "Hello world!")]
|
||||||
[TestCase("add --a 1 --b 2", "3")]
|
[TestCase("sum -v 1 2", "3")]
|
||||||
[TestCase("add --a 2.75 --b 3.6", "6.35")]
|
[TestCase("sum -v 2.75 3.6 4.18", "10.53")]
|
||||||
[TestCase("log --value 100", "2")]
|
[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")]
|
[TestCase("log --value 256 --base 2", "8")]
|
||||||
public async Task Execute_Test(string arguments, string expectedOutput)
|
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
|
||||||
{
|
{
|
||||||
// Act
|
// Arrange & Act
|
||||||
var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync();
|
var result = await Cli.Wrap(DummyFilePath)
|
||||||
|
.SetArguments(arguments)
|
||||||
|
.EnableExitCodeValidation()
|
||||||
|
.EnableStandardErrorValidation()
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(result.ExitCode, Is.Zero);
|
result.StandardOutput.Trim().Should().Be(expectedOutput);
|
||||||
Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput));
|
}
|
||||||
Assert.That(result.StandardError.Trim(), Is.Empty);
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("--version")]
|
||||||
|
public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var result = await Cli.Wrap(DummyFilePath)
|
||||||
|
.SetArguments(arguments)
|
||||||
|
.EnableExitCodeValidation()
|
||||||
|
.EnableStandardErrorValidation()
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.StandardOutput.Trim().Should().Be(DummyVersionText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
42
CliFx.Tests/HelpTextRendererTests.Commands.cs
Normal file
42
CliFx.Tests/HelpTextRendererTests.Commands.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
CliFx.Tests/HelpTextRendererTests.cs
Normal file
105
CliFx.Tests/HelpTextRendererTests.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestObjects
|
|
||||||
{
|
|
||||||
[DefaultCommand]
|
|
||||||
[Command("command")]
|
|
||||||
public class TestCommand : Command
|
|
||||||
{
|
|
||||||
[CommandOption("int", ShortName = 'i', IsRequired = true)]
|
|
||||||
public int IntOption { get; set; } = 24;
|
|
||||||
|
|
||||||
[CommandOption("str", ShortName = 's')]
|
|
||||||
public string StringOption { get; set; } = "foo bar";
|
|
||||||
|
|
||||||
public override ExitCode Execute() => new ExitCode(IntOption, StringOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace CliFx.Tests.TestObjects
|
|
||||||
{
|
|
||||||
public enum TestEnum
|
|
||||||
{
|
|
||||||
Value1,
|
|
||||||
Value2,
|
|
||||||
Value3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
CliFx.sln
28
CliFx.sln
@@ -16,6 +16,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
Readme.md = Readme.md
|
Readme.md = Readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -62,6 +66,30 @@ Global
|
|||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = 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.Build.0 = Debug|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.ActiveCfg = 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.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -2,14 +2,36 @@
|
|||||||
|
|
||||||
namespace CliFx.Attributes
|
namespace CliFx.Attributes
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Annotates a type that defines a command.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class CommandAttribute : Attribute
|
public class CommandAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command name.
|
||||||
|
/// </summary>
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command description, which is used in help text.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
public CommandAttribute(string name)
|
public CommandAttribute(string name)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name; // can be null
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandAttribute()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,63 @@
|
|||||||
|
|
||||||
namespace CliFx.Attributes
|
namespace CliFx.Attributes
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Annotates a property that defines a command option.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandOptionAttribute : Attribute
|
public class CommandOptionAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Option name.
|
||||||
|
/// </summary>
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public char ShortName { get; set; }
|
/// <summary>
|
||||||
|
/// Option short name.
|
||||||
|
/// </summary>
|
||||||
|
public char? ShortName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether an option is required.
|
||||||
|
/// </summary>
|
||||||
public bool IsRequired { get; set; }
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option description, which is used in help text.
|
||||||
|
/// </summary>
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
public CommandOptionAttribute(string name)
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionAttribute(string name, char? shortName)
|
||||||
|
{
|
||||||
|
Name = name; // can be null
|
||||||
|
ShortName = shortName; // can be null
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionAttribute(string name, char shortName)
|
||||||
|
: this(name, (char?) shortName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionAttribute(string name)
|
||||||
|
: this(name, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionAttribute(char shortName)
|
||||||
|
: this(null, shortName)
|
||||||
{
|
{
|
||||||
Name = name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Attributes
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
|
||||||
public class DefaultCommandAttribute : Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,144 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.Reflection;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
public partial class CliApplication : ICliApplication
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICliApplication"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CliApplication : ICliApplication
|
||||||
{
|
{
|
||||||
private readonly ICommandResolver _commandResolver;
|
private readonly ApplicationMetadata _metadata;
|
||||||
|
private readonly ApplicationConfiguration _configuration;
|
||||||
|
|
||||||
public CliApplication(ICommandResolver commandResolver)
|
private readonly IConsole _console;
|
||||||
{
|
private readonly ICommandInputParser _commandInputParser;
|
||||||
_commandResolver = commandResolver;
|
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
||||||
}
|
private readonly ICommandFactory _commandFactory;
|
||||||
|
private readonly ICommandInitializer _commandInitializer;
|
||||||
public CliApplication()
|
private readonly IHelpTextRenderer _helpTextRenderer;
|
||||||
: this(GetDefaultCommandResolver(Assembly.GetCallingAssembly()))
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CliApplication"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CliApplication(ApplicationMetadata metadata, ApplicationConfiguration configuration,
|
||||||
|
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
|
||||||
|
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer)
|
||||||
{
|
{
|
||||||
|
_metadata = metadata.GuardNotNull(nameof(metadata));
|
||||||
|
_configuration = configuration.GuardNotNull(nameof(configuration));
|
||||||
|
|
||||||
|
_console = console.GuardNotNull(nameof(console));
|
||||||
|
_commandInputParser = commandInputParser.GuardNotNull(nameof(commandInputParser));
|
||||||
|
_commandSchemaResolver = commandSchemaResolver.GuardNotNull(nameof(commandSchemaResolver));
|
||||||
|
_commandFactory = commandFactory.GuardNotNull(nameof(commandFactory));
|
||||||
|
_commandInitializer = commandInitializer.GuardNotNull(nameof(commandInitializer));
|
||||||
|
_helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||||
{
|
{
|
||||||
// Resolve and execute command
|
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
|
||||||
var command = _commandResolver.ResolveCommand(commandLineArguments);
|
|
||||||
var exitCode = await command.ExecuteAsync();
|
|
||||||
|
|
||||||
// TODO: print message if error?
|
try
|
||||||
|
{
|
||||||
|
// Get schemas for all available command types
|
||||||
|
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||||
|
|
||||||
return exitCode.Value;
|
// Parse command input from arguments
|
||||||
}
|
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments);
|
||||||
}
|
|
||||||
|
|
||||||
public partial class CliApplication
|
// Find command schema matching the name specified in the input
|
||||||
{
|
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
|
||||||
private static ICommandResolver GetDefaultCommandResolver(Assembly assembly)
|
|
||||||
{
|
|
||||||
var typeProvider = TypeProvider.FromAssembly(assembly);
|
|
||||||
var commandOptionParser = new CommandOptionParser();
|
|
||||||
var commandOptionConverter = new CommandOptionConverter();
|
|
||||||
|
|
||||||
return new CommandResolver(typeProvider, commandOptionParser, commandOptionConverter);
|
// 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)
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// In case we catch a CliFx-specific exception, we want to just show the error message, not the stack trace.
|
||||||
|
// Stack trace isn't very useful to the user if the exception is not really coming from their code.
|
||||||
|
|
||||||
|
// CommandException is the same, but it also lets users specify exit code so we want to return that instead of default.
|
||||||
|
|
||||||
|
var message = ex is CliFxException && !ex.Message.IsNullOrWhiteSpace() ? ex.Message : ex.ToString();
|
||||||
|
var exitCode = ex is CommandException commandEx ? commandEx.ExitCode : ex.HResult;
|
||||||
|
|
||||||
|
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(message));
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
164
CliFx/CliApplicationBuilder.cs
Normal file
164
CliFx/CliApplicationBuilder.cs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICliApplicationBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CliApplicationBuilder : ICliApplicationBuilder
|
||||||
|
{
|
||||||
|
private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
|
||||||
|
|
||||||
|
private string _title;
|
||||||
|
private string _executableName;
|
||||||
|
private string _versionText;
|
||||||
|
private string _description;
|
||||||
|
private IConsole _console;
|
||||||
|
private ICommandFactory _commandFactory;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder AddCommand(Type commandType)
|
||||||
|
{
|
||||||
|
commandType.GuardNotNull(nameof(commandType));
|
||||||
|
|
||||||
|
_commandTypes.Add(commandType);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
|
||||||
|
{
|
||||||
|
commandAssembly.GuardNotNull(nameof(commandAssembly));
|
||||||
|
|
||||||
|
var commandTypes = commandAssembly.ExportedTypes.Where(t => t.Implements(typeof(ICommand)));
|
||||||
|
|
||||||
|
foreach (var commandType in commandTypes)
|
||||||
|
AddCommand(commandType);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseTitle(string title)
|
||||||
|
{
|
||||||
|
_title = title.GuardNotNull(nameof(title));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseExecutableName(string executableName)
|
||||||
|
{
|
||||||
|
_executableName = executableName.GuardNotNull(nameof(executableName));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseVersionText(string versionText)
|
||||||
|
{
|
||||||
|
_versionText = versionText.GuardNotNull(nameof(versionText));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseDescription(string description)
|
||||||
|
{
|
||||||
|
_description = description; // can be null
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseConsole(IConsole console)
|
||||||
|
{
|
||||||
|
_console = console.GuardNotNull(nameof(console));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory)
|
||||||
|
{
|
||||||
|
_commandFactory = factory.GuardNotNull(nameof(factory));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetFallbackValues()
|
||||||
|
{
|
||||||
|
if (_title.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// 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 />
|
||||||
|
public ICliApplication Build()
|
||||||
|
{
|
||||||
|
// Use defaults for required parameters that were not configured
|
||||||
|
SetFallbackValues();
|
||||||
|
|
||||||
|
// Project parameters to expected types
|
||||||
|
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
|
||||||
|
var configuration = new ApplicationConfiguration(_commandTypes.ToArray());
|
||||||
|
|
||||||
|
return new CliApplication(metadata, configuration,
|
||||||
|
_console, new CommandInputParser(), new CommandSchemaResolver(),
|
||||||
|
_commandFactory, new CommandInitializer(), new HelpTextRenderer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class CliApplicationBuilder
|
||||||
|
{
|
||||||
|
private static readonly Lazy<Assembly> LazyEntryAssembly = new Lazy<Assembly>(Assembly.GetEntryAssembly);
|
||||||
|
|
||||||
|
private static Assembly EntryAssembly => LazyEntryAssembly.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>0.0.1</Version>
|
<Version>0.0.2</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Authors>$(Company)</Authors>
|
<Authors>$(Company)</Authors>
|
||||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
||||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx
|
|
||||||
{
|
|
||||||
public abstract class Command
|
|
||||||
{
|
|
||||||
public virtual ExitCode Execute() => throw new InvalidOperationException(
|
|
||||||
"Can't execute command because its execution method is not defined. " +
|
|
||||||
$"Override Execute or ExecuteAsync on {GetType().Name} in order to make it executable.");
|
|
||||||
|
|
||||||
public virtual Task<ExitCode> ExecuteAsync() => Task.FromResult(Execute());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
CliFx/Exceptions/CliFxException.cs
Normal file
26
CliFx/Exceptions/CliFxException.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Domain exception thrown within CliFx.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class CliFxException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected CliFxException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected CliFxException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
CliFx/Exceptions/CommandException.cs
Normal file
45
CliFx/Exceptions/CommandException.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a command cannot proceed with normal execution due to an error.
|
||||||
|
/// Use this exception if you want to report an error that occured during execution of a command.
|
||||||
|
/// This exception also allows specifying exit code which will be returned to the calling process.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandException : CliFxException
|
||||||
|
{
|
||||||
|
private const int DefaultExitCode = -100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process exit code.
|
||||||
|
/// </summary>
|
||||||
|
public int ExitCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandException(string message, Exception innerException, int exitCode = DefaultExitCode)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
ExitCode = exitCode.GuardNotZero(nameof(exitCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandException(string message, int exitCode = DefaultExitCode)
|
||||||
|
: this(message, null, exitCode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandException(int exitCode = DefaultExitCode)
|
||||||
|
: this(null, exitCode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Exceptions
|
|
||||||
{
|
|
||||||
public class CommandResolveException : Exception
|
|
||||||
{
|
|
||||||
public CommandResolveException()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandResolveException(string message)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandResolveException(string message, Exception innerException)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
CliFx/Exceptions/InvalidCommandOptionInputException.cs
Normal file
26
CliFx/Exceptions/InvalidCommandOptionInputException.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a command option can't be converted to target type specified in its schema.
|
||||||
|
/// </summary>
|
||||||
|
public class InvalidCommandOptionInputException : CliFxException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="InvalidCommandOptionInputException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public InvalidCommandOptionInputException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="InvalidCommandOptionInputException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public InvalidCommandOptionInputException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
CliFx/Exceptions/InvalidCommandSchemaException.cs
Normal file
26
CliFx/Exceptions/InvalidCommandSchemaException.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a command schema fails validation.
|
||||||
|
/// </summary>
|
||||||
|
public class InvalidCommandSchemaException : CliFxException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public InvalidCommandSchemaException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public InvalidCommandSchemaException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
CliFx/Exceptions/MissingCommandOptionInputException.cs
Normal file
26
CliFx/Exceptions/MissingCommandOptionInputException.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a required command option was not set.
|
||||||
|
/// </summary>
|
||||||
|
public class MissingCommandOptionInputException : CliFxException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="MissingCommandOptionInputException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MissingCommandOptionInputException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="MissingCommandOptionInputException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MissingCommandOptionInputException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,63 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Reflection;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions for <see cref="CliFx"/>.
|
||||||
|
/// </summary>
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
public static Task<int> RunAsync(this ICliApplication application) =>
|
/// <summary>
|
||||||
application.RunAsync(Environment.GetCommandLineArgs().Skip(1).ToArray());
|
/// Adds multiple commands to the application.
|
||||||
|
/// </summary>
|
||||||
|
public static ICliApplicationBuilder AddCommands(this ICliApplicationBuilder builder, IReadOnlyList<Type> commandTypes)
|
||||||
|
{
|
||||||
|
builder.GuardNotNull(nameof(builder));
|
||||||
|
commandTypes.GuardNotNull(nameof(commandTypes));
|
||||||
|
|
||||||
|
foreach (var commandType in commandTypes)
|
||||||
|
builder.AddCommand(commandType);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from specified assemblies to the application.
|
||||||
|
/// </summary>
|
||||||
|
public static ICliApplicationBuilder AddCommandsFrom(this ICliApplicationBuilder builder, IReadOnlyList<Assembly> commandAssemblies)
|
||||||
|
{
|
||||||
|
builder.GuardNotNull(nameof(builder));
|
||||||
|
commandAssemblies.GuardNotNull(nameof(commandAssemblies));
|
||||||
|
|
||||||
|
foreach (var commandAssembly in commandAssemblies)
|
||||||
|
builder.AddCommandsFrom(commandAssembly);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from calling assembly to the application.
|
||||||
|
/// </summary>
|
||||||
|
public static ICliApplicationBuilder AddCommandsFromThisAssembly(this ICliApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.GuardNotNull(nameof(builder));
|
||||||
|
return builder.AddCommandsFrom(Assembly.GetCallingAssembly());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures application to use specified factory method for creating new instances of <see cref="ICommand"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static ICliApplicationBuilder UseCommandFactory(this ICliApplicationBuilder builder, Func<CommandSchema, ICommand> factoryMethod)
|
||||||
|
{
|
||||||
|
builder.GuardNotNull(nameof(builder));
|
||||||
|
factoryMethod.GuardNotNull(nameof(factoryMethod));
|
||||||
|
|
||||||
|
return builder.UseCommandFactory(new DelegateCommandFactory(factoryMethod));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,14 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point for a command line application.
|
||||||
|
/// </summary>
|
||||||
public interface ICliApplication
|
public interface ICliApplication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs application with specified command line arguments and returns an exit code.
|
||||||
|
/// </summary>
|
||||||
Task<int> RunAsync(IReadOnlyList<string> commandLineArguments);
|
Task<int> RunAsync(IReadOnlyList<string> commandLineArguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
58
CliFx/ICliApplicationBuilder.cs
Normal file
58
CliFx/ICliApplicationBuilder.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an instance of <see cref="ICliApplication"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICliApplicationBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a command of specified type to the application.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder AddCommand(Type commandType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds commands from specified assembly to the application.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets application title, which appears in the help text.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseTitle(string title);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets application executable name, which appears in the help text.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseExecutableName(string executableName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets application version text, which appears in the help text and when the user requests version information.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseVersionText(string versionText);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets application description, which appears in the help text.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseDescription(string description);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures application to use specified implementation of <see cref="IConsole"/>.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseConsole(IConsole console);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures application to use specified implementation of <see cref="ICommandFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="ICliApplication"/> using configured parameters.
|
||||||
|
/// Default values are used in place of parameters that were not specified.
|
||||||
|
/// </summary>
|
||||||
|
ICliApplication Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
CliFx/ICommand.cs
Normal file
17
CliFx/ICommand.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Point of interaction between a user and command line interface.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes command using specified implementation of <see cref="IConsole"/>.
|
||||||
|
/// This method is called when the command is invoked by a user through command line interface.
|
||||||
|
/// </summary>
|
||||||
|
Task ExecuteAsync(IConsole console);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
|
|
||||||
namespace CliFx.Internal
|
|
||||||
{
|
|
||||||
internal partial class CommandOptionProperty
|
|
||||||
{
|
|
||||||
private readonly PropertyInfo _property;
|
|
||||||
|
|
||||||
public Type Type => _property.PropertyType;
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public char ShortName { get; }
|
|
||||||
|
|
||||||
public bool IsRequired { get; }
|
|
||||||
|
|
||||||
public string Description { get; }
|
|
||||||
|
|
||||||
public CommandOptionProperty(PropertyInfo property, string name, char shortName, bool isRequired, string description)
|
|
||||||
{
|
|
||||||
_property = property;
|
|
||||||
Name = name;
|
|
||||||
ShortName = shortName;
|
|
||||||
IsRequired = isRequired;
|
|
||||||
Description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetValue(Command command, object value) => _property.SetValue(command, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandOptionProperty
|
|
||||||
{
|
|
||||||
public static bool IsValid(PropertyInfo property) => property.IsDefined(typeof(CommandOptionAttribute));
|
|
||||||
|
|
||||||
public static CommandOptionProperty Initialize(PropertyInfo property)
|
|
||||||
{
|
|
||||||
if (!IsValid(property))
|
|
||||||
throw new InvalidOperationException($"[{property.Name}] is not a valid command option property.");
|
|
||||||
|
|
||||||
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
|
|
||||||
|
|
||||||
return new CommandOptionProperty(property, attribute.Name, attribute.ShortName, attribute.IsRequired,
|
|
||||||
attribute.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
|
|
||||||
namespace CliFx.Internal
|
|
||||||
{
|
|
||||||
internal partial class CommandType
|
|
||||||
{
|
|
||||||
private readonly Type _type;
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public bool IsDefault { get; }
|
|
||||||
|
|
||||||
public CommandType(Type type, string name, bool isDefault)
|
|
||||||
{
|
|
||||||
_type = type;
|
|
||||||
Name = name;
|
|
||||||
IsDefault = isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<CommandOptionProperty> GetOptionProperties() => _type.GetProperties()
|
|
||||||
.Where(CommandOptionProperty.IsValid)
|
|
||||||
.Select(CommandOptionProperty.Initialize);
|
|
||||||
|
|
||||||
public Command Activate() => (Command) Activator.CreateInstance(_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class CommandType
|
|
||||||
{
|
|
||||||
public static bool IsValid(Type type) =>
|
|
||||||
// Derives from Command
|
|
||||||
type.IsDerivedFrom(typeof(Command)) &&
|
|
||||||
// Marked with DefaultCommandAttribute or CommandAttribute
|
|
||||||
(type.IsDefined(typeof(DefaultCommandAttribute)) || type.IsDefined(typeof(CommandAttribute)));
|
|
||||||
|
|
||||||
public static CommandType Initialize(Type type)
|
|
||||||
{
|
|
||||||
if (!IsValid(type))
|
|
||||||
throw new InvalidOperationException($"[{type.Name}] is not a valid command type.");
|
|
||||||
|
|
||||||
var name = type.GetCustomAttribute<CommandAttribute>()?.Name;
|
|
||||||
var isDefault = type.IsDefined(typeof(DefaultCommandAttribute));
|
|
||||||
|
|
||||||
return new CommandType(type, name, isDefault);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<CommandType> GetCommandTypes(IEnumerable<Type> types) => types.Where(IsValid).Select(Initialize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal
|
||||||
{
|
{
|
||||||
@@ -7,51 +10,60 @@ namespace CliFx.Internal
|
|||||||
{
|
{
|
||||||
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
|
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
|
||||||
|
|
||||||
public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static string Repeat(this char c, int count) => new string(c, count);
|
||||||
|
|
||||||
|
public static string AsString(this char c) => c.Repeat(1);
|
||||||
|
|
||||||
|
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
|
||||||
|
|
||||||
|
public static string SubstringUntilLast(this string s, string sub,
|
||||||
|
StringComparison comparison = StringComparison.Ordinal)
|
||||||
{
|
{
|
||||||
var index = s.IndexOf(sub, comparison);
|
var index = s.LastIndexOf(sub, comparison);
|
||||||
return index < 0 ? s : s.Substring(0, index);
|
return index < 0 ? s : s.Substring(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string SubstringAfter(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
||||||
{
|
builder.Length > 0 ? builder.Append(value) : builder;
|
||||||
var index = s.IndexOf(sub, comparison);
|
|
||||||
return index < 0 ? string.Empty : s.Substring(index + sub.Length, s.Length - index - sub.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
||||||
dic.TryGetValue(key, out var result) ? result : default;
|
dic.TryGetValue(key, out var result) ? result : default;
|
||||||
|
|
||||||
public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
|
||||||
{
|
|
||||||
while (s.StartsWith(sub, comparison))
|
|
||||||
s = s.Substring(sub.Length);
|
|
||||||
|
|
||||||
return s;
|
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value)
|
||||||
|
{
|
||||||
|
foreach (var i in source)
|
||||||
|
yield return i;
|
||||||
|
|
||||||
|
yield return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string TrimEnd(this string s, string sub, StringComparison comparison = StringComparison.Ordinal)
|
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||||
{
|
|
||||||
while (s.EndsWith(sub, comparison))
|
|
||||||
s = s.Substring(0, s.Length - sub.Length);
|
|
||||||
|
|
||||||
return s;
|
public static Type GetEnumerableUnderlyingType(this Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(IEnumerable))
|
||||||
|
return typeof(object);
|
||||||
|
|
||||||
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
|
return type.GetGenericArguments().FirstOrDefault();
|
||||||
|
|
||||||
|
return type.GetInterfaces()
|
||||||
|
.Select(GetEnumerableUnderlyingType)
|
||||||
|
.ExceptNull()
|
||||||
|
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
||||||
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
|
public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType)
|
||||||
|
|
||||||
public static bool IsDerivedFrom(this Type type, Type baseType)
|
|
||||||
{
|
{
|
||||||
var currentType = type;
|
var sourceAsCollection = source as ICollection ?? source.ToArray();
|
||||||
while (currentType != null)
|
|
||||||
{
|
|
||||||
if (currentType == baseType)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
currentType = currentType.BaseType;
|
var array = Array.CreateInstance(elementType, sourceAsCollection.Count);
|
||||||
}
|
sourceAsCollection.CopyTo(array, 0);
|
||||||
|
|
||||||
return false;
|
return array;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
CliFx/Internal/Guards.cs
Normal file
13
CliFx/Internal/Guards.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Internal
|
||||||
|
{
|
||||||
|
internal static class Guards
|
||||||
|
{
|
||||||
|
public static T GuardNotNull<T>(this T o, string argName = null) where T : class =>
|
||||||
|
o ?? throw new ArgumentNullException(argName);
|
||||||
|
|
||||||
|
public static int GuardNotZero(this int i, string argName = null) =>
|
||||||
|
i != 0 ? i : throw new ArgumentException("Cannot be zero.", argName);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
CliFx/Models/ApplicationConfiguration.cs
Normal file
25
CliFx/Models/ApplicationConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration of an application.
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command types defined in the application.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Type> CommandTypes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes)
|
||||||
|
{
|
||||||
|
CommandTypes = commandTypes.GuardNotNull(nameof(commandTypes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
CliFx/Models/ApplicationMetadata.cs
Normal file
41
CliFx/Models/ApplicationMetadata.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata associated with an application.
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Application title.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application executable name.
|
||||||
|
/// </summary>
|
||||||
|
public string ExecutableName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application version text.
|
||||||
|
/// </summary>
|
||||||
|
public string VersionText { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application description.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationMetadata(string title, string executableName, string versionText, string description)
|
||||||
|
{
|
||||||
|
Title = title.GuardNotNull(nameof(title));
|
||||||
|
ExecutableName = executableName.GuardNotNull(nameof(executableName));
|
||||||
|
VersionText = versionText.GuardNotNull(nameof(versionText));
|
||||||
|
Description = description; // can be null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
CliFx/Models/CommandInput.cs
Normal file
81
CliFx/Models/CommandInput.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed command line input.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CommandInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specified command name.
|
||||||
|
/// Can be null if command was not specified.
|
||||||
|
/// </summary>
|
||||||
|
public string CommandName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specified options.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<CommandOptionInput> Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options)
|
||||||
|
{
|
||||||
|
CommandName = commandName; // can be null
|
||||||
|
Options = options.GuardNotNull(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInput(IReadOnlyList<CommandOptionInput> options)
|
||||||
|
: this(null, options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInput(string commandName)
|
||||||
|
: this(commandName, new CommandOptionInput[0])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInput()
|
||||||
|
: this(null, new CommandOptionInput[0])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
if (!CommandName.IsNullOrWhiteSpace())
|
||||||
|
buffer.Append(CommandName);
|
||||||
|
|
||||||
|
foreach (var option in Options)
|
||||||
|
{
|
||||||
|
buffer.AppendIfNotEmpty(' ');
|
||||||
|
buffer.Append(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class CommandInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Empty input.
|
||||||
|
/// </summary>
|
||||||
|
public static CommandInput Empty { get; } = new CommandInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
CliFx/Models/CommandOptionInput.cs
Normal file
73
CliFx/Models/CommandOptionInput.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed option from command line input.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandOptionInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specified option alias.
|
||||||
|
/// </summary>
|
||||||
|
public string Alias { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specified values.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Values { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionInput(string alias, IReadOnlyList<string> values)
|
||||||
|
{
|
||||||
|
Alias = alias.GuardNotNull(nameof(alias));
|
||||||
|
Values = values.GuardNotNull(nameof(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionInput(string alias, string value)
|
||||||
|
: this(alias, new[] {value})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionInput"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionInput(string alias)
|
||||||
|
: this(alias, new string[0])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
buffer.Append(Alias.Length > 1 ? "--" : "-");
|
||||||
|
buffer.Append(Alias);
|
||||||
|
|
||||||
|
foreach (var value in Values)
|
||||||
|
{
|
||||||
|
buffer.AppendIfNotEmpty(' ');
|
||||||
|
|
||||||
|
var isEscaped = value.Contains(" ");
|
||||||
|
|
||||||
|
if (isEscaped)
|
||||||
|
buffer.Append('"');
|
||||||
|
|
||||||
|
buffer.Append(value);
|
||||||
|
|
||||||
|
if (isEscaped)
|
||||||
|
buffer.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
CliFx/Models/CommandOptionSchema.cs
Normal file
83
CliFx/Models/CommandOptionSchema.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Schema of a defined command option.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CommandOptionSchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Underlying property.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyInfo Property { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option short name.
|
||||||
|
/// </summary>
|
||||||
|
public char? ShortName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether an option is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option description.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionSchema(PropertyInfo property, string name, char? shortName, bool isRequired, string description)
|
||||||
|
{
|
||||||
|
Property = property; // can be null
|
||||||
|
Name = name; // can be null
|
||||||
|
ShortName = shortName; // can be null
|
||||||
|
IsRequired = isRequired;
|
||||||
|
Description = description; // can be null
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
if (IsRequired)
|
||||||
|
buffer.Append('*');
|
||||||
|
|
||||||
|
if (!Name.IsNullOrWhiteSpace())
|
||||||
|
buffer.Append(Name);
|
||||||
|
|
||||||
|
if (!Name.IsNullOrWhiteSpace() && ShortName != null)
|
||||||
|
buffer.Append('|');
|
||||||
|
|
||||||
|
if (ShortName != null)
|
||||||
|
buffer.Append(ShortName);
|
||||||
|
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class CommandOptionSchema
|
||||||
|
{
|
||||||
|
// Here we define some built-in options.
|
||||||
|
// This is probably a bit hacky but I couldn't come up with a better solution given this architecture.
|
||||||
|
// We define them here to serve as a single source of truth, because they are used...
|
||||||
|
// ...in CliApplication (when reading) and HelpTextRenderer (when writing).
|
||||||
|
|
||||||
|
internal static CommandOptionSchema HelpOption { get; } =
|
||||||
|
new CommandOptionSchema(null, "help", 'h', false, "Shows help text.");
|
||||||
|
|
||||||
|
internal static CommandOptionSchema VersionOption { get; } =
|
||||||
|
new CommandOptionSchema(null, "version", null, false, "Shows version information.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Internal;
|
|
||||||
|
|
||||||
namespace CliFx.Models
|
|
||||||
{
|
|
||||||
public partial class CommandOptionSet
|
|
||||||
{
|
|
||||||
public string CommandName { get; }
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, string> Options { get; }
|
|
||||||
|
|
||||||
public CommandOptionSet(string commandName, IReadOnlyDictionary<string, string> options)
|
|
||||||
{
|
|
||||||
CommandName = commandName;
|
|
||||||
Options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandOptionSet(IReadOnlyDictionary<string, string> options)
|
|
||||||
: this(null, options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandOptionSet(string commandName)
|
|
||||||
: this(commandName, new Dictionary<string, string>())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => !CommandName.IsNullOrWhiteSpace()
|
|
||||||
? $"{CommandName} / {Options.Count} option(s)"
|
|
||||||
: $"{Options.Count} option(s)";
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class CommandOptionSet
|
|
||||||
{
|
|
||||||
public static CommandOptionSet Empty { get; } = new CommandOptionSet(new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
CliFx/Models/CommandSchema.cs
Normal file
69
CliFx/Models/CommandSchema.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Schema of a defined command.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CommandSchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Underlying type.
|
||||||
|
/// </summary>
|
||||||
|
public Type Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command description.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command options.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<CommandOptionSchema> Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandOptionSchema> options)
|
||||||
|
{
|
||||||
|
Type = type; // can be null
|
||||||
|
Name = name; // can be null
|
||||||
|
Description = description; // can be null
|
||||||
|
Options = options.GuardNotNull(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
if (!Name.IsNullOrWhiteSpace())
|
||||||
|
buffer.Append(Name);
|
||||||
|
|
||||||
|
foreach (var option in Options)
|
||||||
|
{
|
||||||
|
buffer.AppendIfNotEmpty(' ');
|
||||||
|
buffer.Append('[');
|
||||||
|
buffer.Append(option);
|
||||||
|
buffer.Append(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class CommandSchema
|
||||||
|
{
|
||||||
|
internal static CommandSchema StubDefaultCommand { get; } =
|
||||||
|
new CommandSchema(null, null, null, new CommandOptionSchema[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CliFx.Models
|
|
||||||
{
|
|
||||||
public partial class ExitCode
|
|
||||||
{
|
|
||||||
public int Value { get; }
|
|
||||||
|
|
||||||
public string Message { get; }
|
|
||||||
|
|
||||||
public bool IsSuccess => Value == 0;
|
|
||||||
|
|
||||||
public ExitCode(int value, string message = null)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ExitCode
|
|
||||||
{
|
|
||||||
public static ExitCode Success { get; } = new ExitCode(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
144
CliFx/Models/Extensions.cs
Normal file
144
CliFx/Models/Extensions.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions for <see cref="Models"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a command that has specified name, or null if not found.
|
||||||
|
/// </summary>
|
||||||
|
public static CommandSchema FindByName(this IReadOnlyList<CommandSchema> commandSchemas, string commandName)
|
||||||
|
{
|
||||||
|
commandSchemas.GuardNotNull(nameof(commandSchemas));
|
||||||
|
|
||||||
|
// If looking for default command, don't compare names directly
|
||||||
|
// ...because null and empty are both valid names for default command
|
||||||
|
if (commandName.IsNullOrWhiteSpace())
|
||||||
|
return commandSchemas.FirstOrDefault(c => c.IsDefault());
|
||||||
|
|
||||||
|
return commandSchemas.FirstOrDefault(c => string.Equals(c.Name, commandName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds parent command to the command that has specified name, or null if not found.
|
||||||
|
/// </summary>
|
||||||
|
public static CommandSchema FindParent(this IReadOnlyList<CommandSchema> commandSchemas, string commandName)
|
||||||
|
{
|
||||||
|
commandSchemas.GuardNotNull(nameof(commandSchemas));
|
||||||
|
|
||||||
|
// If command has no name, it's the default command so it doesn't have a parent
|
||||||
|
if (commandName.IsNullOrWhiteSpace())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Repeatedly cut off individual words from the name until we find a command with that name
|
||||||
|
var temp = commandName;
|
||||||
|
while (temp.Contains(" "))
|
||||||
|
{
|
||||||
|
temp = temp.SubstringUntilLast(" ");
|
||||||
|
|
||||||
|
var parent = commandSchemas.FindByName(temp);
|
||||||
|
if (parent != null)
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no parent is matched by name, then the parent is the default command
|
||||||
|
return commandSchemas.FirstOrDefault(c => c.IsDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether an option schema matches specified alias.
|
||||||
|
/// </summary>
|
||||||
|
public static bool MatchesAlias(this CommandOptionSchema optionSchema, string alias)
|
||||||
|
{
|
||||||
|
optionSchema.GuardNotNull(nameof(optionSchema));
|
||||||
|
alias.GuardNotNull(nameof(alias));
|
||||||
|
|
||||||
|
// Compare against name. Case is ignored.
|
||||||
|
var matchesByName =
|
||||||
|
!optionSchema.Name.IsNullOrWhiteSpace() &&
|
||||||
|
string.Equals(optionSchema.Name, alias, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Compare against short name. Case is NOT ignored.
|
||||||
|
var matchesByShortName =
|
||||||
|
optionSchema.ShortName != null &&
|
||||||
|
alias.Length == 1 && alias[0] == optionSchema.ShortName;
|
||||||
|
|
||||||
|
return matchesByName || matchesByShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an option that matches specified alias, or null if not found.
|
||||||
|
/// </summary>
|
||||||
|
public static CommandOptionSchema FindByAlias(this IReadOnlyList<CommandOptionSchema> optionSchemas, string alias)
|
||||||
|
{
|
||||||
|
optionSchemas.GuardNotNull(nameof(optionSchemas));
|
||||||
|
alias.GuardNotNull(nameof(alias));
|
||||||
|
|
||||||
|
return optionSchemas.FirstOrDefault(o => o.MatchesAlias(alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets valid aliases for the option.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string> GetAliases(this CommandOptionSchema optionSchema)
|
||||||
|
{
|
||||||
|
var result = new List<string>(2);
|
||||||
|
|
||||||
|
if (!optionSchema.Name.IsNullOrWhiteSpace())
|
||||||
|
result.Add(optionSchema.Name);
|
||||||
|
|
||||||
|
if (optionSchema.ShortName != null)
|
||||||
|
result.Add(optionSchema.ShortName.Value.AsString());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether a command was specified in the input.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsCommandSpecified(this CommandInput commandInput)
|
||||||
|
{
|
||||||
|
commandInput.GuardNotNull(nameof(commandInput));
|
||||||
|
return !commandInput.CommandName.IsNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether help was requested in the input.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsHelpRequested(this CommandInput commandInput)
|
||||||
|
{
|
||||||
|
commandInput.GuardNotNull(nameof(commandInput));
|
||||||
|
|
||||||
|
var firstOption = commandInput.Options.FirstOrDefault();
|
||||||
|
|
||||||
|
return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether version information was requested in the input.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsVersionRequested(this CommandInput commandInput)
|
||||||
|
{
|
||||||
|
commandInput.GuardNotNull(nameof(commandInput));
|
||||||
|
|
||||||
|
var firstOption = commandInput.Options.FirstOrDefault();
|
||||||
|
|
||||||
|
return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this command is the default command, i.e. without a name.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDefault(this CommandSchema commandSchema)
|
||||||
|
{
|
||||||
|
commandSchema.GuardNotNull(nameof(commandSchema));
|
||||||
|
return commandSchema.Name.IsNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
CliFx/Models/HelpTextSource.cs
Normal file
38
CliFx/Models/HelpTextSource.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Source information used to generate help text.
|
||||||
|
/// </summary>
|
||||||
|
public class HelpTextSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Application metadata.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationMetadata ApplicationMetadata { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schemas of commands available in the application.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schema of the command for which help text is to be generated.
|
||||||
|
/// </summary>
|
||||||
|
public CommandSchema TargetCommandSchema { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="HelpTextSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
public HelpTextSource(ApplicationMetadata applicationMetadata,
|
||||||
|
IReadOnlyList<CommandSchema> availableCommandSchemas,
|
||||||
|
CommandSchema targetCommandSchema)
|
||||||
|
{
|
||||||
|
ApplicationMetadata = applicationMetadata.GuardNotNull(nameof(applicationMetadata));
|
||||||
|
AvailableCommandSchemas = availableCommandSchemas.GuardNotNull(nameof(availableCommandSchemas));
|
||||||
|
TargetCommandSchema = targetCommandSchema.GuardNotNull(nameof(targetCommandSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
CliFx/Services/CommandFactory.cs
Normal file
19
CliFx/Services/CommandFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICommandFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandFactory : ICommandFactory
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICommand CreateCommand(CommandSchema commandSchema)
|
||||||
|
{
|
||||||
|
commandSchema.GuardNotNull(nameof(commandSchema));
|
||||||
|
return (ICommand) Activator.CreateInstance(commandSchema.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
CliFx/Services/CommandInitializer.cs
Normal file
68
CliFx/Services/CommandInitializer.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICommandInitializer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandInitializer : ICommandInitializer
|
||||||
|
{
|
||||||
|
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter)
|
||||||
|
{
|
||||||
|
_commandOptionInputConverter = commandOptionInputConverter.GuardNotNull(nameof(commandOptionInputConverter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInitializer()
|
||||||
|
: this(new CommandOptionInputConverter())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
||||||
|
{
|
||||||
|
command.GuardNotNull(nameof(command));
|
||||||
|
commandSchema.GuardNotNull(nameof(commandSchema));
|
||||||
|
commandInput.GuardNotNull(nameof(commandInput));
|
||||||
|
|
||||||
|
// Keep track of unset required options to report an error at a later stage
|
||||||
|
var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList();
|
||||||
|
|
||||||
|
// Set command options
|
||||||
|
foreach (var optionInput in commandInput.Options)
|
||||||
|
{
|
||||||
|
// Find matching option schema for this option input
|
||||||
|
var optionSchema = commandSchema.Options.FindByAlias(optionInput.Alias);
|
||||||
|
if (optionSchema == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Convert option to the type of the underlying property
|
||||||
|
var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
|
||||||
|
|
||||||
|
// Set value of the underlying property
|
||||||
|
optionSchema.Property.SetValue(command, convertedValue);
|
||||||
|
|
||||||
|
// Mark this required option as set
|
||||||
|
if (optionSchema.IsRequired)
|
||||||
|
unsetRequiredOptions.Remove(optionSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if any of the required options were not set
|
||||||
|
if (unsetRequiredOptions.Any())
|
||||||
|
{
|
||||||
|
var unsetRequiredOptionNames = unsetRequiredOptions.Select(o => o.GetAliases().FirstOrDefault()).JoinToString(", ");
|
||||||
|
throw new MissingCommandOptionInputException($"One or more required options were not set: {unsetRequiredOptionNames}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
CliFx/Services/CommandInputParser.cs
Normal file
71
CliFx/Services/CommandInputParser.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICommandInputParser"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandInputParser : ICommandInputParser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
|
||||||
|
{
|
||||||
|
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
|
||||||
|
|
||||||
|
var commandNameBuilder = new StringBuilder();
|
||||||
|
var optionsDic = new Dictionary<string, List<string>>();
|
||||||
|
|
||||||
|
var lastOptionAlias = "";
|
||||||
|
|
||||||
|
foreach (var commandLineArgument in commandLineArguments)
|
||||||
|
{
|
||||||
|
// Encountered option name
|
||||||
|
if (commandLineArgument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Extract option alias
|
||||||
|
lastOptionAlias = commandLineArgument.Substring(2);
|
||||||
|
|
||||||
|
if (!optionsDic.ContainsKey(lastOptionAlias))
|
||||||
|
optionsDic[lastOptionAlias] = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encountered short option name or multiple thereof
|
||||||
|
else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Handle stacked options
|
||||||
|
foreach (var c in commandLineArgument.Substring(1))
|
||||||
|
{
|
||||||
|
// Extract option alias
|
||||||
|
lastOptionAlias = c.AsString();
|
||||||
|
|
||||||
|
if (!optionsDic.ContainsKey(lastOptionAlias))
|
||||||
|
optionsDic[lastOptionAlias] = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encountered command name or part thereof
|
||||||
|
else if (lastOptionAlias.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
commandNameBuilder.AppendIfNotEmpty(' ');
|
||||||
|
commandNameBuilder.Append(commandLineArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encountered option value
|
||||||
|
else if (!lastOptionAlias.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
optionsDic[lastOptionAlias].Add(commandLineArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
|
||||||
|
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
|
||||||
|
|
||||||
|
return new CommandInput(commandName, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using CliFx.Internal;
|
|
||||||
|
|
||||||
namespace CliFx.Services
|
|
||||||
{
|
|
||||||
public class CommandOptionConverter : ICommandOptionConverter
|
|
||||||
{
|
|
||||||
private readonly IFormatProvider _formatProvider;
|
|
||||||
|
|
||||||
public CommandOptionConverter(IFormatProvider formatProvider)
|
|
||||||
{
|
|
||||||
_formatProvider = formatProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandOptionConverter()
|
|
||||||
: this(CultureInfo.InvariantCulture)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertOption(string value, Type targetType)
|
|
||||||
{
|
|
||||||
// String or object
|
|
||||||
if (targetType == typeof(string) || targetType == typeof(object))
|
|
||||||
return value;
|
|
||||||
|
|
||||||
// Bool
|
|
||||||
if (targetType == typeof(bool))
|
|
||||||
return value.IsNullOrWhiteSpace() || bool.Parse(value);
|
|
||||||
|
|
||||||
// DateTime
|
|
||||||
if (targetType == typeof(DateTime))
|
|
||||||
return DateTime.Parse(value, _formatProvider);
|
|
||||||
|
|
||||||
// DateTimeOffset
|
|
||||||
if (targetType == typeof(DateTimeOffset))
|
|
||||||
return DateTimeOffset.Parse(value, _formatProvider);
|
|
||||||
|
|
||||||
// TimeSpan
|
|
||||||
if (targetType == typeof(TimeSpan))
|
|
||||||
return TimeSpan.Parse(value, _formatProvider);
|
|
||||||
|
|
||||||
// Enum
|
|
||||||
if (targetType.IsEnum)
|
|
||||||
return Enum.Parse(targetType, value, true);
|
|
||||||
|
|
||||||
// Nullable
|
|
||||||
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
|
|
||||||
if (nullableUnderlyingType != null)
|
|
||||||
return !value.IsNullOrWhiteSpace() ? ConvertOption(value, nullableUnderlyingType) : null;
|
|
||||||
|
|
||||||
// All other types
|
|
||||||
return Convert.ChangeType(value, targetType, _formatProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
185
CliFx/Services/CommandOptionInputConverter.cs
Normal file
185
CliFx/Services/CommandOptionInputConverter.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICommandOptionInputConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CommandOptionInputConverter : ICommandOptionInputConverter
|
||||||
|
{
|
||||||
|
private readonly IFormatProvider _formatProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionInputConverter(IFormatProvider formatProvider)
|
||||||
|
{
|
||||||
|
_formatProvider = formatProvider.GuardNotNull(nameof(formatProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandOptionInputConverter()
|
||||||
|
: this(CultureInfo.InvariantCulture)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ConvertValue(string value, Type targetType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// String or object
|
||||||
|
if (targetType == typeof(string) || targetType == typeof(object))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
// Bool
|
||||||
|
if (targetType == typeof(bool))
|
||||||
|
return value.IsNullOrWhiteSpace() || bool.Parse(value);
|
||||||
|
|
||||||
|
// Char
|
||||||
|
if (targetType == typeof(char))
|
||||||
|
return value.Single();
|
||||||
|
|
||||||
|
// Sbyte
|
||||||
|
if (targetType == typeof(sbyte))
|
||||||
|
return sbyte.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Byte
|
||||||
|
if (targetType == typeof(byte))
|
||||||
|
return byte.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Short
|
||||||
|
if (targetType == typeof(short))
|
||||||
|
return short.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Ushort
|
||||||
|
if (targetType == typeof(ushort))
|
||||||
|
return ushort.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Int
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
return int.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Uint
|
||||||
|
if (targetType == typeof(uint))
|
||||||
|
return uint.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Long
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
return long.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Ulong
|
||||||
|
if (targetType == typeof(ulong))
|
||||||
|
return ulong.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Float
|
||||||
|
if (targetType == typeof(float))
|
||||||
|
return float.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Double
|
||||||
|
if (targetType == typeof(double))
|
||||||
|
return double.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Decimal
|
||||||
|
if (targetType == typeof(decimal))
|
||||||
|
return decimal.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// DateTime
|
||||||
|
if (targetType == typeof(DateTime))
|
||||||
|
return DateTime.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// DateTimeOffset
|
||||||
|
if (targetType == typeof(DateTimeOffset))
|
||||||
|
return DateTimeOffset.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// TimeSpan
|
||||||
|
if (targetType == typeof(TimeSpan))
|
||||||
|
return TimeSpan.Parse(value, _formatProvider);
|
||||||
|
|
||||||
|
// Enum
|
||||||
|
if (targetType.IsEnum)
|
||||||
|
return Enum.Parse(targetType, value, true);
|
||||||
|
|
||||||
|
// Nullable
|
||||||
|
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
|
||||||
|
if (nullableUnderlyingType != null)
|
||||||
|
return !value.IsNullOrWhiteSpace() ? ConvertValue(value, nullableUnderlyingType) : null;
|
||||||
|
|
||||||
|
// Has a constructor that accepts a single string
|
||||||
|
var stringConstructor = GetStringConstructor(targetType);
|
||||||
|
if (stringConstructor != null)
|
||||||
|
return stringConstructor.Invoke(new object[] {value});
|
||||||
|
|
||||||
|
// Has a static parse method that accepts a single string and a format provider
|
||||||
|
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
||||||
|
if (parseMethodWithFormatProvider != null)
|
||||||
|
return parseMethodWithFormatProvider.Invoke(null, new object[] {value, _formatProvider});
|
||||||
|
|
||||||
|
// Has a static parse method that accepts a single string
|
||||||
|
var parseMethod = GetStaticParseMethod(targetType);
|
||||||
|
if (parseMethod != null)
|
||||||
|
return parseMethod.Invoke(null, new object[] {value});
|
||||||
|
|
||||||
|
throw new InvalidCommandOptionInputException($"Can't convert value [{value}] to type [{targetType}].");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidCommandOptionInputException($"Can't convert value [{value}] to type [{targetType}].", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
|
||||||
|
{
|
||||||
|
optionInput.GuardNotNull(nameof(optionInput));
|
||||||
|
targetType.GuardNotNull(nameof(targetType));
|
||||||
|
|
||||||
|
// Single value
|
||||||
|
if (optionInput.Values.Count <= 1)
|
||||||
|
{
|
||||||
|
var value = optionInput.Values.SingleOrDefault();
|
||||||
|
return ConvertValue(value, targetType);
|
||||||
|
}
|
||||||
|
// Multiple values
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Determine underlying type of elements inside the target collection type
|
||||||
|
var underlyingType = targetType.GetEnumerableUnderlyingType() ?? typeof(object);
|
||||||
|
|
||||||
|
// Convert values to that type
|
||||||
|
var convertedValues = optionInput.Values.Select(v => ConvertValue(v, underlyingType)).ToNonGenericArray(underlyingType);
|
||||||
|
var convertedValuesType = convertedValues.GetType();
|
||||||
|
|
||||||
|
// Assignable from array of values (e.g. T[], IReadOnlyList<T>, IEnumerable<T>)
|
||||||
|
if (targetType.IsAssignableFrom(convertedValuesType))
|
||||||
|
return convertedValues;
|
||||||
|
|
||||||
|
// Has a constructor that accepts an array of values (e.g. HashSet<T>, List<T>)
|
||||||
|
var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType});
|
||||||
|
if (arrayConstructor != null)
|
||||||
|
return arrayConstructor.Invoke(new object[] {convertedValues});
|
||||||
|
|
||||||
|
throw new InvalidCommandOptionInputException(
|
||||||
|
$"Can't convert sequence of values [{optionInput.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class CommandOptionInputConverter
|
||||||
|
{
|
||||||
|
private static ConstructorInfo GetStringConstructor(Type type) => type.GetConstructor(new[] {typeof(string)});
|
||||||
|
|
||||||
|
private static MethodInfo GetStaticParseMethod(Type type) =>
|
||||||
|
type.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] {typeof(string)}, null);
|
||||||
|
|
||||||
|
private static MethodInfo GetStaticParseMethodWithFormatProvider(Type type) =>
|
||||||
|
type.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] {typeof(string), typeof(IFormatProvider)}, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using CliFx.Internal;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx.Services
|
|
||||||
{
|
|
||||||
public class CommandOptionParser : ICommandOptionParser
|
|
||||||
{
|
|
||||||
public CommandOptionSet ParseOptions(IReadOnlyList<string> commandLineArguments)
|
|
||||||
{
|
|
||||||
// Initialize command name placeholder
|
|
||||||
string commandName = null;
|
|
||||||
|
|
||||||
// Initialize options
|
|
||||||
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Keep track of the last option's name
|
|
||||||
string optionName = null;
|
|
||||||
|
|
||||||
// Loop through all arguments
|
|
||||||
var isFirstArgument = true;
|
|
||||||
foreach (var commandLineArgument in commandLineArguments)
|
|
||||||
{
|
|
||||||
// Option name
|
|
||||||
if (commandLineArgument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Extract option name (skip 2 chars)
|
|
||||||
optionName = commandLineArgument.Substring(2);
|
|
||||||
options[optionName] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Short option name
|
|
||||||
else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase) && commandLineArgument.Length == 2)
|
|
||||||
{
|
|
||||||
// Extract option name (skip 1 char)
|
|
||||||
optionName = commandLineArgument.Substring(1);
|
|
||||||
options[optionName] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple stacked short options
|
|
||||||
else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
optionName = null;
|
|
||||||
foreach (var c in commandLineArgument.Substring(1))
|
|
||||||
{
|
|
||||||
options[c.ToString(CultureInfo.InvariantCulture)] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command name
|
|
||||||
else if (isFirstArgument)
|
|
||||||
{
|
|
||||||
commandName = commandLineArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option value
|
|
||||||
else if (!optionName.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
// ReSharper disable once AssignNullToNotNullAttribute
|
|
||||||
options[optionName] = commandLineArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
isFirstArgument = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandOptionSet(commandName, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Internal;
|
|
||||||
|
|
||||||
namespace CliFx.Services
|
|
||||||
{
|
|
||||||
public class CommandResolver : ICommandResolver
|
|
||||||
{
|
|
||||||
private readonly ITypeProvider _typeProvider;
|
|
||||||
private readonly ICommandOptionParser _commandOptionParser;
|
|
||||||
private readonly ICommandOptionConverter _commandOptionConverter;
|
|
||||||
|
|
||||||
public CommandResolver(ITypeProvider typeProvider,
|
|
||||||
ICommandOptionParser commandOptionParser, ICommandOptionConverter commandOptionConverter)
|
|
||||||
{
|
|
||||||
_typeProvider = typeProvider;
|
|
||||||
_commandOptionParser = commandOptionParser;
|
|
||||||
_commandOptionConverter = commandOptionConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<CommandType> GetCommandTypes() => CommandType.GetCommandTypes(_typeProvider.GetTypes());
|
|
||||||
|
|
||||||
private CommandType GetDefaultCommandType()
|
|
||||||
{
|
|
||||||
// Get command types marked as default
|
|
||||||
var defaultCommandTypes = GetCommandTypes().Where(t => t.IsDefault).ToArray();
|
|
||||||
|
|
||||||
// If there's only one type - return
|
|
||||||
if (defaultCommandTypes.Length == 1)
|
|
||||||
return defaultCommandTypes.Single();
|
|
||||||
|
|
||||||
// If there are multiple - throw
|
|
||||||
if (defaultCommandTypes.Length > 1)
|
|
||||||
{
|
|
||||||
throw new CommandResolveException(
|
|
||||||
"Can't resolve default command because there is more than one command marked as default. " +
|
|
||||||
$"Make sure you apply {nameof(DefaultCommandAttribute)} only to one command.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there aren't any - throw
|
|
||||||
throw new CommandResolveException(
|
|
||||||
"Can't resolve default command because there are no commands marked as default. " +
|
|
||||||
$"Apply {nameof(DefaultCommandAttribute)} to the default command.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandType GetCommandType(string name)
|
|
||||||
{
|
|
||||||
// Get command types with given name
|
|
||||||
var matchingCommandTypes =
|
|
||||||
GetCommandTypes().Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray();
|
|
||||||
|
|
||||||
// If there's only one type - return
|
|
||||||
if (matchingCommandTypes.Length == 1)
|
|
||||||
return matchingCommandTypes.Single();
|
|
||||||
|
|
||||||
// If there are multiple - throw
|
|
||||||
if (matchingCommandTypes.Length > 1)
|
|
||||||
{
|
|
||||||
throw new CommandResolveException(
|
|
||||||
$"Can't resolve command because there is more than one command named [{name}]. " +
|
|
||||||
"Make sure all command names are unique and keep in mind that comparison is case-insensitive.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there aren't any - throw
|
|
||||||
throw new CommandResolveException(
|
|
||||||
$"Can't resolve command because none of the commands is named [{name}]. " +
|
|
||||||
$"Apply {nameof(CommandAttribute)} to give command a name.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Command ResolveCommand(IReadOnlyList<string> commandLineArguments)
|
|
||||||
{
|
|
||||||
var optionSet = _commandOptionParser.ParseOptions(commandLineArguments);
|
|
||||||
|
|
||||||
// Get command type
|
|
||||||
var commandType = !optionSet.CommandName.IsNullOrWhiteSpace()
|
|
||||||
? GetCommandType(optionSet.CommandName)
|
|
||||||
: GetDefaultCommandType();
|
|
||||||
|
|
||||||
// Activate command
|
|
||||||
var command = commandType.Activate();
|
|
||||||
|
|
||||||
// Set command options
|
|
||||||
foreach (var property in commandType.GetOptionProperties())
|
|
||||||
{
|
|
||||||
// If option set contains this property - set value
|
|
||||||
if (optionSet.Options.TryGetValue(property.Name, out var value) ||
|
|
||||||
optionSet.Options.TryGetValue(property.ShortName.ToString(CultureInfo.InvariantCulture), out value))
|
|
||||||
{
|
|
||||||
var convertedValue = _commandOptionConverter.ConvertOption(value, property.Type);
|
|
||||||
property.SetValue(command, convertedValue);
|
|
||||||
}
|
|
||||||
// If the property is missing but it's required - throw
|
|
||||||
else if (property.IsRequired)
|
|
||||||
{
|
|
||||||
throw new CommandResolveException(
|
|
||||||
$"Can't resolve command [{optionSet.CommandName}] because required property [{property.Name}] is not set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
127
CliFx/Services/CommandSchemaResolver.cs
Normal file
127
CliFx/Services/CommandSchemaResolver.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ICommandSchemaResolver"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||||
|
{
|
||||||
|
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
|
||||||
|
{
|
||||||
|
var attribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||||
|
|
||||||
|
if (attribute == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new CommandOptionSchema(optionProperty,
|
||||||
|
attribute.Name,
|
||||||
|
attribute.ShortName,
|
||||||
|
attribute.IsRequired,
|
||||||
|
attribute.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandSchema GetCommandSchema(Type commandType)
|
||||||
|
{
|
||||||
|
// Attribute is optional for commands in order to reduce runtime rule complexity
|
||||||
|
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||||
|
|
||||||
|
var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
|
||||||
|
|
||||||
|
return new CommandSchema(commandType,
|
||||||
|
attribute?.Name,
|
||||||
|
attribute?.Description,
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
|
||||||
|
{
|
||||||
|
commandTypes.GuardNotNull(nameof(commandTypes));
|
||||||
|
|
||||||
|
// Get command schemas
|
||||||
|
var commandSchemas = commandTypes.Select(GetCommandSchema).ToArray();
|
||||||
|
|
||||||
|
// Throw if there are no commands defined
|
||||||
|
if (!commandSchemas.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidCommandSchemaException("There are no commands defined.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if there are multiple commands with the same name
|
||||||
|
var nonUniqueCommandNames = commandSchemas
|
||||||
|
.Select(c => c.Name)
|
||||||
|
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(g => g.Count() >= 2)
|
||||||
|
.SelectMany(g => g)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var commandName in nonUniqueCommandNames)
|
||||||
|
{
|
||||||
|
throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace()
|
||||||
|
? $"There are multiple commands defined with name [{commandName}]."
|
||||||
|
: "There are multiple default commands defined.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if there are commands that don't implement ICommand
|
||||||
|
var nonImplementedCommandNames = commandSchemas
|
||||||
|
.Where(c => !c.Type.Implements(typeof(ICommand)))
|
||||||
|
.Select(c => c.Name)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var commandName in nonImplementedCommandNames)
|
||||||
|
{
|
||||||
|
throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace()
|
||||||
|
? $"Command [{commandName}] doesn't implement ICommand."
|
||||||
|
: "Default command doesn't implement ICommand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if there are multiple options with the same name inside the same command
|
||||||
|
foreach (var commandSchema in commandSchemas)
|
||||||
|
{
|
||||||
|
var nonUniqueOptionNames = commandSchema.Options
|
||||||
|
.Where(o => !o.Name.IsNullOrWhiteSpace())
|
||||||
|
.Select(o => o.Name)
|
||||||
|
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(g => g.Count() >= 2)
|
||||||
|
.SelectMany(g => g)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var optionName in nonUniqueOptionNames)
|
||||||
|
{
|
||||||
|
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||||
|
? $"There are multiple options defined with name [{optionName}] on command [{commandSchema.Name}]."
|
||||||
|
: $"There are multiple options defined with name [{optionName}] on default command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonUniqueOptionShortNames = commandSchema.Options
|
||||||
|
.Where(o => o.ShortName != null)
|
||||||
|
.Select(o => o.ShortName.Value)
|
||||||
|
.GroupBy(i => i)
|
||||||
|
.Where(g => g.Count() >= 2)
|
||||||
|
.SelectMany(g => g)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var optionShortName in nonUniqueOptionShortNames)
|
||||||
|
{
|
||||||
|
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||||
|
? $"There are multiple options defined with short name [{optionShortName}] on command [{commandSchema.Name}]."
|
||||||
|
: $"There are multiple options defined with short name [{optionShortName}] on default command.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandSchemas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
CliFx/Services/DelegateCommandFactory.cs
Normal file
29
CliFx/Services/DelegateCommandFactory.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of <see cref="ICommandFactory"/> that uses a factory method to create commands.
|
||||||
|
/// </summary>
|
||||||
|
public class DelegateCommandFactory : ICommandFactory
|
||||||
|
{
|
||||||
|
private readonly Func<CommandSchema, ICommand> _factoryMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="DelegateCommandFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DelegateCommandFactory(Func<CommandSchema, ICommand> factoryMethod)
|
||||||
|
{
|
||||||
|
_factoryMethod = factoryMethod.GuardNotNull(nameof(factoryMethod));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICommand CreateCommand(CommandSchema commandSchema)
|
||||||
|
{
|
||||||
|
commandSchema.GuardNotNull(nameof(commandSchema));
|
||||||
|
return _factoryMethod(commandSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,54 @@
|
|||||||
namespace CliFx.Services
|
using System;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions for <see cref="Services"/>
|
||||||
|
/// </summary>
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
public static Command ResolveCommand(this ICommandResolver commandResolver) => commandResolver.ResolveCommand(new string[0]);
|
/// <summary>
|
||||||
|
/// Sets console foreground color, executes specified action, and sets the color back to the original value.
|
||||||
|
/// </summary>
|
||||||
|
public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
|
||||||
|
{
|
||||||
|
console.GuardNotNull(nameof(console));
|
||||||
|
action.GuardNotNull(nameof(action));
|
||||||
|
|
||||||
|
var lastColor = console.ForegroundColor;
|
||||||
|
console.ForegroundColor = foregroundColor;
|
||||||
|
|
||||||
|
action();
|
||||||
|
|
||||||
|
console.ForegroundColor = lastColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets console background color, executes specified action, and sets the color back to the original value.
|
||||||
|
/// </summary>
|
||||||
|
public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
|
||||||
|
{
|
||||||
|
console.GuardNotNull(nameof(console));
|
||||||
|
action.GuardNotNull(nameof(action));
|
||||||
|
|
||||||
|
var lastColor = console.BackgroundColor;
|
||||||
|
console.BackgroundColor = backgroundColor;
|
||||||
|
|
||||||
|
action();
|
||||||
|
|
||||||
|
console.BackgroundColor = lastColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
|
||||||
|
/// </summary>
|
||||||
|
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action)
|
||||||
|
{
|
||||||
|
console.GuardNotNull(nameof(console));
|
||||||
|
action.GuardNotNull(nameof(action));
|
||||||
|
|
||||||
|
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
287
CliFx/Services/HelpTextRenderer.cs
Normal file
287
CliFx/Services/HelpTextRenderer.cs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="IHelpTextRenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class HelpTextRenderer : IHelpTextRenderer
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RenderHelpText(IConsole console, HelpTextSource source)
|
||||||
|
{
|
||||||
|
console.GuardNotNull(nameof(console));
|
||||||
|
source.GuardNotNull(nameof(source));
|
||||||
|
|
||||||
|
// Track position
|
||||||
|
var column = 0;
|
||||||
|
var row = 0;
|
||||||
|
|
||||||
|
// Get built-in option schemas (help and version)
|
||||||
|
var builtInOptionSchemas = new List<CommandOptionSchema> {CommandOptionSchema.HelpOption};
|
||||||
|
if (source.TargetCommandSchema.IsDefault())
|
||||||
|
builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
|
||||||
|
|
||||||
|
// Get child command schemas
|
||||||
|
var childCommandSchemas = source.AvailableCommandSchemas
|
||||||
|
.Where(c => source.AvailableCommandSchemas.FindParent(c.Name) == source.TargetCommandSchema)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Define helper functions
|
||||||
|
|
||||||
|
bool IsEmpty() => column == 0 && row == 0;
|
||||||
|
|
||||||
|
void Render(string text)
|
||||||
|
{
|
||||||
|
console.Output.Write(text);
|
||||||
|
|
||||||
|
column += text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderNewLine()
|
||||||
|
{
|
||||||
|
console.Output.WriteLine();
|
||||||
|
|
||||||
|
column = 0;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderMargin(int lines = 1)
|
||||||
|
{
|
||||||
|
if (!IsEmpty())
|
||||||
|
{
|
||||||
|
for (var i = 0; i < lines; i++)
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderIndent(int spaces = 2)
|
||||||
|
{
|
||||||
|
Render(' '.Repeat(spaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderColumnIndent(int spaces = 20, int margin = 2)
|
||||||
|
{
|
||||||
|
if (column + margin >= spaces)
|
||||||
|
{
|
||||||
|
RenderNewLine();
|
||||||
|
RenderIndent(spaces);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderIndent(spaces - column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderWithColor(string text, ConsoleColor foregroundColor)
|
||||||
|
{
|
||||||
|
console.WithForegroundColor(foregroundColor, () => Render(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderWithColors(string text, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
|
||||||
|
{
|
||||||
|
console.WithColors(foregroundColor, backgroundColor, () => Render(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderHeader(string text)
|
||||||
|
{
|
||||||
|
RenderWithColors(text, ConsoleColor.Black, ConsoleColor.DarkGray);
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderApplicationInfo()
|
||||||
|
{
|
||||||
|
if (!source.TargetCommandSchema.IsDefault())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Title and version
|
||||||
|
RenderWithColor(source.ApplicationMetadata.Title, ConsoleColor.Yellow);
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor(source.ApplicationMetadata.VersionText, ConsoleColor.Yellow);
|
||||||
|
RenderNewLine();
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (!source.ApplicationMetadata.Description.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
Render(source.ApplicationMetadata.Description);
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderDescription()
|
||||||
|
{
|
||||||
|
if (source.TargetCommandSchema.Description.IsNullOrWhiteSpace())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RenderHeader("Description");
|
||||||
|
|
||||||
|
// Description
|
||||||
|
RenderIndent();
|
||||||
|
Render(source.TargetCommandSchema.Description);
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderUsage()
|
||||||
|
{
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RenderHeader("Usage");
|
||||||
|
|
||||||
|
// Exe name
|
||||||
|
RenderIndent();
|
||||||
|
Render(source.ApplicationMetadata.ExecutableName);
|
||||||
|
|
||||||
|
// Command name
|
||||||
|
if (!source.TargetCommandSchema.IsDefault())
|
||||||
|
{
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child command
|
||||||
|
if (childCommandSchemas.Any())
|
||||||
|
{
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor("[command]", ConsoleColor.Cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor("[options]", ConsoleColor.White);
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderOptions()
|
||||||
|
{
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RenderHeader("Options");
|
||||||
|
|
||||||
|
// Options
|
||||||
|
foreach (var optionSchema in source.TargetCommandSchema.Options.Concat(builtInOptionSchemas))
|
||||||
|
{
|
||||||
|
// Is required
|
||||||
|
if (optionSchema.IsRequired)
|
||||||
|
{
|
||||||
|
RenderWithColor("* ", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderIndent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short name
|
||||||
|
if (optionSchema.ShortName != null)
|
||||||
|
{
|
||||||
|
RenderWithColor($"-{optionSchema.ShortName}", ConsoleColor.White);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delimiter
|
||||||
|
if (!optionSchema.Name.IsNullOrWhiteSpace() && optionSchema.ShortName != null)
|
||||||
|
{
|
||||||
|
Render("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (!optionSchema.Name.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
RenderWithColor($"--{optionSchema.Name}", ConsoleColor.White);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (!optionSchema.Description.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
RenderColumnIndent();
|
||||||
|
Render(optionSchema.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderChildCommands()
|
||||||
|
{
|
||||||
|
if (!childCommandSchemas.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RenderHeader("Commands");
|
||||||
|
|
||||||
|
// Child commands
|
||||||
|
foreach (var childCommandSchema in childCommandSchemas)
|
||||||
|
{
|
||||||
|
var relativeCommandName = GetRelativeCommandName(childCommandSchema, source.TargetCommandSchema);
|
||||||
|
|
||||||
|
// Name
|
||||||
|
RenderIndent();
|
||||||
|
RenderWithColor(relativeCommandName, ConsoleColor.Cyan);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (!childCommandSchema.Description.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
RenderColumnIndent();
|
||||||
|
Render(childCommandSchema.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Child command help tip
|
||||||
|
Render("You can run `");
|
||||||
|
Render(source.ApplicationMetadata.ExecutableName);
|
||||||
|
|
||||||
|
if (!source.TargetCommandSchema.IsDefault())
|
||||||
|
{
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor("[command]", ConsoleColor.Cyan);
|
||||||
|
|
||||||
|
Render(" ");
|
||||||
|
RenderWithColor("--help", ConsoleColor.White);
|
||||||
|
|
||||||
|
Render("` to show help on a specific command.");
|
||||||
|
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset color just in case
|
||||||
|
console.ResetColor();
|
||||||
|
|
||||||
|
// Render everything
|
||||||
|
RenderApplicationInfo();
|
||||||
|
RenderDescription();
|
||||||
|
RenderUsage();
|
||||||
|
RenderOptions();
|
||||||
|
RenderChildCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class HelpTextRenderer
|
||||||
|
{
|
||||||
|
private static string GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) =>
|
||||||
|
parentCommandSchema.Name.IsNullOrWhiteSpace()
|
||||||
|
? commandSchema.Name
|
||||||
|
: commandSchema.Name.Substring(parentCommandSchema.Name.Length + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
CliFx/Services/ICommandFactory.cs
Normal file
16
CliFx/Services/ICommandFactory.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes new instances of <see cref="ICommand"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="ICommand"/> with specified schema.
|
||||||
|
/// </summary>
|
||||||
|
ICommand CreateCommand(CommandSchema commandSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
CliFx/Services/ICommandInitializer.cs
Normal file
15
CliFx/Services/ICommandInitializer.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Populates <see cref="ICommand"/> instances with input according to its schema.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandInitializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Populates an instance of <see cref="ICommand"/> with specified input according to specified schema.
|
||||||
|
/// </summary>
|
||||||
|
void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
CliFx/Services/ICommandInputParser.cs
Normal file
16
CliFx/Services/ICommandInputParser.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandInputParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses specified command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Services
|
|
||||||
{
|
|
||||||
public interface ICommandOptionConverter
|
|
||||||
{
|
|
||||||
object ConvertOption(string value, Type targetType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
CliFx/Services/ICommandOptionInputConverter.cs
Normal file
16
CliFx/Services/ICommandOptionInputConverter.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts input command options.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandOptionInputConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an option to specified target type.
|
||||||
|
/// </summary>
|
||||||
|
object ConvertOptionInput(CommandOptionInput optionInput, Type targetType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Models;
|
|
||||||
|
|
||||||
namespace CliFx.Services
|
|
||||||
{
|
|
||||||
public interface ICommandOptionParser
|
|
||||||
{
|
|
||||||
CommandOptionSet ParseOptions(IReadOnlyList<string> commandLineArguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user