9 Commits
0.0.6 ... 0.0.8

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

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

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

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

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

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
@@ -25,14 +24,14 @@ namespace CliFx.Demo.Commands
public DateTimeOffset Published { get; set; } public DateTimeOffset Published { get; set; }
[CommandOption("isbn", 'n', Description = "Book ISBN.")] [CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; set; } public Isbn? Isbn { get; set; }
public BookAddCommand(LibraryService libraryService) public BookAddCommand(LibraryService libraryService)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) public Task ExecuteAsync(IConsole console)
{ {
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set // To make the demo simpler, we will just generate random publish date and ISBN if they were not set
if (Published == default) if (Published == default)

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
@@ -21,7 +20,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) public Task ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
@@ -17,7 +16,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) public Task ExecuteAsync(IConsole console)
{ {
var library = _libraryService.GetLibrary(); var library = _libraryService.GetLibrary();

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
@@ -20,7 +19,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) public Task ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);

View File

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

View File

@@ -21,8 +21,8 @@ namespace CliFx.Tests
builder builder
.AddCommand(typeof(HelloWorldDefaultCommand)) .AddCommand(typeof(HelloWorldDefaultCommand))
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly) .AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
.AddCommands(new[] { typeof(HelloWorldDefaultCommand) }) .AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
.AddCommandsFrom(new[] { typeof(HelloWorldDefaultCommand).Assembly }) .AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.AllowDebugMode() .AllowDebugMode()
.AllowPreviewMode() .AllowPreviewMode()
@@ -31,7 +31,7 @@ namespace CliFx.Tests
.UseVersionText("test") .UseVersionText("test")
.UseDescription("test") .UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null)) .UseConsole(new VirtualConsole(TextWriter.Null))
.UseCommandFactory(schema => (ICommand)Activator.CreateInstance(schema.Type)) .UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!)
.UseCommandOptionInputConverter(new CommandOptionInputConverter()) .UseCommandOptionInputConverter(new CommandOptionInputConverter())
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub()) .UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
.Build(); .Build();

View File

@@ -3,6 +3,7 @@ using NUnit.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.Stubs; using CliFx.Tests.Stubs;
@@ -18,104 +19,104 @@ namespace CliFx.Tests
private static IEnumerable<TestCaseData> GetTestCases_RunAsync() private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) }, new[] {typeof(HelloWorldDefaultCommand)},
new string[0], new string[0],
"Hello world." "Hello world."
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "concat", "-i", "foo", "-i", "bar", "-s", " " }, new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
"foo bar" "foo bar"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "concat", "-i", "one", "two", "three", "-s", ", " }, new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
"one, two, three" "one, two, three"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(DivideCommand) }, new[] {typeof(DivideCommand)},
new[] { "div", "-D", "24", "-d", "8" }, new[] {"div", "-D", "24", "-d", "8"},
"3" "3"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) }, new[] {typeof(HelloWorldDefaultCommand)},
new[] { "--version" }, new[] {"--version"},
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "--version" }, new[] {"--version"},
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) }, new[] {typeof(HelloWorldDefaultCommand)},
new[] { "-h" }, new[] {"-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(HelloWorldDefaultCommand) }, new[] {typeof(HelloWorldDefaultCommand)},
new[] { "--help" }, new[] {"--help"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new string[0], new string[0],
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "-h" }, new[] {"-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "--help" }, new[] {"--help"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "concat", "-h" }, new[] {"concat", "-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ExceptionCommand) }, new[] {typeof(ExceptionCommand)},
new[] { "exc", "-h" }, new[] {"exc", "-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) }, new[] {typeof(CommandExceptionCommand)},
new[] { "exc", "-h" }, new[] {"exc", "-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "[preview]" }, new[] {"[preview]"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ExceptionCommand) }, new[] {typeof(ExceptionCommand)},
new[] { "exc", "[preview]" }, new[] {"exc", "[preview]"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "concat", "[preview]", "-o", "value" }, new[] {"concat", "[preview]", "-o", "value"},
null null
); );
} }
@@ -129,38 +130,38 @@ namespace CliFx.Tests
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ConcatCommand) }, new[] {typeof(ConcatCommand)},
new[] { "non-existing" }, new[] {"non-existing"},
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(ExceptionCommand) }, new[] {typeof(ExceptionCommand)},
new[] { "exc" }, new[] {"exc"},
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) }, new[] {typeof(CommandExceptionCommand)},
new[] { "exc" }, new[] {"exc"},
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) }, new[] {typeof(CommandExceptionCommand)},
new[] { "exc" }, new[] {"exc"},
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) }, new[] {typeof(CommandExceptionCommand)},
new[] { "exc", "-m", "foo bar" }, new[] {"exc", "-m", "foo bar"},
"foo bar", null "foo bar", null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] { typeof(CommandExceptionCommand) }, new[] {typeof(CommandExceptionCommand)},
new[] { "exc", "-m", "foo bar", "-c", "666" }, new[] {"exc", "-m", "foo bar", "-c", "666"},
"foo bar", 666 "foo bar", 666
); );
} }
@@ -168,94 +169,92 @@ namespace CliFx.Tests
[Test] [Test]
[TestCaseSource(nameof(GetTestCases_RunAsync))] [TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
string expectedStdOut = null) string? expectedStdOut = null)
{ {
// Arrange // Arrange
using (var stdoutStream = new StringWriter()) await using var stdoutStream = new StringWriter();
{
var console = new VirtualConsole(stdoutStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var console = new VirtualConsole(stdoutStream);
.AddCommands(commandTypes) var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
.UseVersionText(TestVersionText)
.UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build();
// Act var application = new CliApplicationBuilder()
var exitCode = await application.RunAsync(commandLineArguments); .AddCommands(commandTypes)
var stdOut = stdoutStream.ToString().Trim(); .UseVersionText(TestVersionText)
.UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build();
// Assert // Act
exitCode.Should().Be(0); var exitCode = await application.RunAsync(commandLineArguments);
var stdOut = stdoutStream.ToString().Trim();
if (expectedStdOut != null) // Assert
stdOut.Should().Be(expectedStdOut); exitCode.Should().Be(0);
else
stdOut.Should().NotBeNullOrWhiteSpace(); if (expectedStdOut != null)
} stdOut.Should().Be(expectedStdOut);
else
stdOut.Should().NotBeNullOrWhiteSpace();
} }
[Test] [Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
string expectedStdErr = null, int? expectedExitCode = null) string? expectedStdErr = null, int? expectedExitCode = null)
{ {
// Arrange // Arrange
using (var stderrStream = new StringWriter()) await using var stderrStream = new StringWriter();
{
var console = new VirtualConsole(TextWriter.Null, stderrStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var console = new VirtualConsole(TextWriter.Null, stderrStream);
.AddCommands(commandTypes) var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
.UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console)
.Build();
// Act var application = new CliApplicationBuilder()
var exitCode = await application.RunAsync(commandLineArguments); .AddCommands(commandTypes)
var stderr = stderrStream.ToString().Trim(); .UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console)
.Build();
// Assert // Act
if (expectedExitCode != null) var exitCode = await application.RunAsync(commandLineArguments);
exitCode.Should().Be(expectedExitCode); var stderr = stderrStream.ToString().Trim();
else
exitCode.Should().NotBe(0);
if (expectedStdErr != null) // Assert
stderr.Should().Be(expectedStdErr); if (expectedExitCode != null)
else exitCode.Should().Be(expectedExitCode);
stderr.Should().NotBeNullOrWhiteSpace(); else
} exitCode.Should().NotBe(0);
if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
else
stderr.Should().NotBeNullOrWhiteSpace();
} }
[Test] [Test]
public async Task RunAsync_Cancellation_Test() public async Task RunAsync_Cancellation_Test()
{ {
// Arrange // Arrange
using (var stdoutStream = new StringWriter()) using var cancellationTokenSource = new CancellationTokenSource();
{ await using var stdoutStream = new StringWriter();
var console = new VirtualConsole(stdoutStream);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var args = new[] { "cancel" };
// Act var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token);
var runTask = application.RunAsync(args);
console.Cancel();
var exitCode = await runTask.ConfigureAwait(false);
var stdOut = stdoutStream.ToString().Trim();
// Assert var application = new CliApplicationBuilder()
exitCode.Should().Be(-2146233029); .AddCommand(typeof(CancellableCommand))
stdOut.Should().Be("Printed"); .UseConsole(console)
} .Build();
var args = new[] {"cancel"};
// Act
var runTask = application.RunAsync(args);
cancellationTokenSource.Cancel();
var exitCode = await runTask.ConfigureAwait(false);
var stdOut = stdoutStream.ToString().Trim();
// Assert
exitCode.Should().Be(-2146233029);
stdOut.Should().Be("Printed");
} }
} }
} }

View File

@@ -1,23 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net46</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat> <CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput> <CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.8.0" /> <PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="coverlet.msbuild" Version="2.6.3"> <PackageReference Include="coverlet.msbuild" Version="2.7.0" PrivateAssets="all" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -8,6 +8,7 @@ using CliFx.Models;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
using CliFx.Tests.Stubs; using CliFx.Tests.Stubs;
using System.IO;
namespace CliFx.Tests.Services namespace CliFx.Tests.Services
{ {
@@ -106,7 +107,7 @@ namespace CliFx.Tests.Services
new EnvironmentVariableWithoutCollectionPropertyCommand(), new EnvironmentVariableWithoutCollectionPropertyCommand(),
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)), GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables), new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = "A;B;C;" } new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
); );
} }

View File

@@ -18,7 +18,7 @@ namespace CliFx.Tests.Services
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand() private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)), new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type!)!),
GetCommandSchema(typeof(HelloWorldDefaultCommand)) GetCommandSchema(typeof(HelloWorldDefaultCommand))
); );
} }

View File

@@ -93,17 +93,16 @@ namespace CliFx.Tests.Services
IReadOnlyList<string> expectedSubstrings) IReadOnlyList<string> expectedSubstrings)
{ {
// Arrange // Arrange
using (var stdout = new StringWriter()) using var stdout = new StringWriter();
{
var console = new VirtualConsole(stdout);
var renderer = new HelpTextRenderer();
// Act var console = new VirtualConsole(stdout);
renderer.RenderHelpText(console, source); var renderer = new HelpTextRenderer();
// Assert // Act
stdout.ToString().Should().ContainAll(expectedSubstrings); renderer.RenderHelpText(console, source);
}
// Assert
stdout.ToString().Should().ContainAll(expectedSubstrings);
} }
} }
} }

View File

@@ -14,30 +14,29 @@ namespace CliFx.Tests.Services
public void All_Smoke_Test() public void All_Smoke_Test()
{ {
// Arrange // Arrange
using (var stdin = new StringReader("hello world")) using var stdin = new StringReader("hello world");
using (var stdout = new StringWriter()) using var stdout = new StringWriter();
using (var stderr = new StringWriter()) using var stderr = new StringWriter();
{
var console = new VirtualConsole(stdin, stdout, stderr);
// Act var console = new VirtualConsole(stdin, stdout, stderr);
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert // Act
console.Input.Should().BeSameAs(stdin); console.ResetColor();
console.Input.Should().NotBeSameAs(Console.In); console.ForegroundColor = ConsoleColor.DarkMagenta;
console.IsInputRedirected.Should().BeTrue(); console.BackgroundColor = ConsoleColor.DarkMagenta;
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out); // Assert
console.IsOutputRedirected.Should().BeTrue(); console.Input.Should().BeSameAs(stdin);
console.Error.Should().BeSameAs(stderr); console.Input.Should().NotBeSameAs(Console.In);
console.Error.Should().NotBeSameAs(Console.Error); console.IsInputRedirected.Should().BeTrue();
console.IsErrorRedirected.Should().BeTrue(); console.Output.Should().BeSameAs(stdout);
console.ForegroundColor.Should().NotBe(Console.ForegroundColor); console.Output.Should().NotBeSameAs(Console.Out);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor); console.IsOutputRedirected.Should().BeTrue();
} console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -9,13 +8,13 @@ namespace CliFx.Tests.TestCommands
[Command("cancel")] [Command("cancel")]
public class CancellableCommand : ICommand public class CancellableCommand : ICommand
{ {
public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) public async Task ExecuteAsync(IConsole console)
{ {
await Task.Yield(); await Task.Yield();
console.Output.WriteLine("Printed"); console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(1), console.GetCancellationToken()).ConfigureAwait(false);
console.Output.WriteLine("Never printed"); console.Output.WriteLine("Never printed");
} }

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -9,11 +8,11 @@ namespace CliFx.Tests.TestCommands
public class DuplicateOptionNamesCommand : ICommand public class DuplicateOptionNamesCommand : ICommand
{ {
[CommandOption("fruits")] [CommandOption("fruits")]
public string Apples { get; set; } public string? Apples { get; set; }
[CommandOption("fruits")] [CommandOption("fruits")]
public string Oranges { get; set; } public string? Oranges { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -9,11 +8,11 @@ namespace CliFx.Tests.TestCommands
public class DuplicateOptionShortNamesCommand : ICommand public class DuplicateOptionShortNamesCommand : ICommand
{ {
[CommandOption('f')] [CommandOption('f')]
public string Apples { get; set; } public string? Apples { get; set; }
[CommandOption('f')] [CommandOption('f')]
public string Oranges { get; set; } public string? Oranges { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -9,8 +8,8 @@ namespace CliFx.Tests.TestCommands
public class EnvironmentVariableCommand : ICommand public class EnvironmentVariableCommand : ICommand
{ {
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] [CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string Option { get; set; } public string? Option { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -9,8 +8,8 @@ namespace CliFx.Tests.TestCommands
public class HelpSubCommand : ICommand public class HelpSubCommand : ICommand
{ {
[CommandOption("option-e", 'e', Description = "OptionE description.")] [CommandOption("option-e", 'e', Description = "OptionE description.")]
public string OptionE { get; set; } public string? OptionE { get; set; }
public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }
} }

View File

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

View File

@@ -17,41 +17,39 @@ namespace CliFx.Tests.Utilities
// Arrange // Arrange
var formatProvider = CultureInfo.InvariantCulture; var formatProvider = CultureInfo.InvariantCulture;
using (var stdout = new StringWriter(formatProvider)) using var stdout = new StringWriter(formatProvider);
{
var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); var console = new VirtualConsole(TextReader.Null, false, stdout, false, TextWriter.Null, false);
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray(); var ticker = console.CreateProgressTicker();
// Act var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
foreach (var progress in progressValues) var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
ticker.Report(progress);
// Assert // Act
stdout.ToString().Should().ContainAll(progressStringValues); foreach (var progress in progressValues)
} ticker.Report(progress);
// Assert
stdout.ToString().Should().ContainAll(progressStringValues);
} }
[Test] [Test]
public void Report_Redirected_Test() public void Report_Redirected_Test()
{ {
// Arrange // Arrange
using (var stdout = new StringWriter()) using var stdout = new StringWriter();
{
var console = new VirtualConsole(stdout);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); var console = new VirtualConsole(stdout);
var ticker = console.CreateProgressTicker();
// Act var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert // Act
stdout.ToString().Should().BeEmpty(); foreach (var progress in progressValues)
} ticker.Report(progress);
// Assert
stdout.ToString().Should().BeEmpty();
} }
} }
} }

View File

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

View File

@@ -10,11 +10,13 @@ namespace CliFx.Attributes
{ {
/// <summary> /// <summary>
/// Option name. /// Option name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// </summary> /// </summary>
public string Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// Option short name. /// Option short name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// </summary> /// </summary>
public char? ShortName { get; } public char? ShortName { get; }
@@ -26,27 +28,27 @@ namespace CliFx.Attributes
/// <summary> /// <summary>
/// Option description, which is used in help text. /// Option description, which is used in help text.
/// </summary> /// </summary>
public string Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified. /// Optional environment variable name that will be used as fallback value if no option value is specified.
/// </summary> /// </summary>
public string EnvironmentVariableName { get; set; } public string? EnvironmentVariableName { get; set; }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(string name, char? shortName) private CommandOptionAttribute(string? name, char? shortName)
{ {
Name = name; // can be null Name = name;
ShortName = shortName; // can be null ShortName = shortName;
} }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(string name, char shortName) public CommandOptionAttribute(string name, char shortName)
: this(name, (char?)shortName) : this(name, (char?) shortName)
{ {
} }
@@ -62,7 +64,7 @@ namespace CliFx.Attributes
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(char shortName) public CommandOptionAttribute(char shortName)
: this(null, shortName) : this(null, (char?) shortName)
{ {
} }
} }

View File

@@ -32,15 +32,15 @@ namespace CliFx
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver, IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer) ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer)
{ {
_metadata = metadata.GuardNotNull(nameof(metadata)); _metadata = metadata;
_configuration = configuration.GuardNotNull(nameof(configuration)); _configuration = configuration;
_console = console.GuardNotNull(nameof(console)); _console = console;
_commandInputParser = commandInputParser.GuardNotNull(nameof(commandInputParser)); _commandInputParser = commandInputParser;
_commandSchemaResolver = commandSchemaResolver.GuardNotNull(nameof(commandSchemaResolver)); _commandSchemaResolver = commandSchemaResolver;
_commandFactory = commandFactory.GuardNotNull(nameof(commandFactory)); _commandFactory = commandFactory;
_commandInitializer = commandInitializer.GuardNotNull(nameof(commandInitializer)); _commandInitializer = commandInitializer;
_helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer)); _helpTextRenderer = helpTextRenderer;
} }
private async Task<int?> HandleDebugDirectiveAsync(CommandInput commandInput) private async Task<int?> HandleDebugDirectiveAsync(CommandInput commandInput)
@@ -117,7 +117,7 @@ namespace CliFx
} }
private int? HandleHelpOption(CommandInput commandInput, private int? HandleHelpOption(CommandInput commandInput,
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema targetCommandSchema) IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema? targetCommandSchema)
{ {
// Help should be rendered if it was requested, or when executing a command which isn't defined // Help should be rendered if it was requested, or when executing a command which isn't defined
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null; var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null;
@@ -171,7 +171,7 @@ namespace CliFx
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput); _commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// Execute command // Execute command
await command.ExecuteAsync(_console, _console.CancellationToken); await command.ExecuteAsync(_console);
// Finish the chain with exit code 0 // Finish the chain with exit code 0
return 0; return 0;
@@ -180,8 +180,6 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments) public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{ {
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
try try
{ {
// Parse command input from arguments // Parse command input from arguments
@@ -199,7 +197,7 @@ namespace CliFx
HandlePreviewDirective(commandInput) ?? HandlePreviewDirective(commandInput) ??
HandleVersionOption(commandInput) ?? HandleVersionOption(commandInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ?? HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
await HandleCommandExecutionAsync(commandInput, targetCommandSchema); await HandleCommandExecutionAsync(commandInput, targetCommandSchema!);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -207,7 +205,7 @@ namespace CliFx
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions. // Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
// Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException // Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
if (!ex.Message.IsNullOrWhiteSpace() && (ex is CliFxException || ex is CommandException)) if (!string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException))
{ {
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message)); _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message));
} }

View File

@@ -19,20 +19,18 @@ namespace CliFx
private bool _isDebugModeAllowed = true; private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true; private bool _isPreviewModeAllowed = true;
private string _title; private string? _title;
private string _executableName; private string? _executableName;
private string _versionText; private string? _versionText;
private string _description; private string? _description;
private IConsole _console; private IConsole? _console;
private ICommandFactory _commandFactory; private ICommandFactory? _commandFactory;
private ICommandOptionInputConverter _commandOptionInputConverter; private ICommandOptionInputConverter? _commandOptionInputConverter;
private IEnvironmentVariablesProvider _environmentVariablesProvider; private IEnvironmentVariablesProvider? _environmentVariablesProvider;
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder AddCommand(Type commandType) public ICliApplicationBuilder AddCommand(Type commandType)
{ {
commandType.GuardNotNull(nameof(commandType));
_commandTypes.Add(commandType); _commandTypes.Add(commandType);
return this; return this;
@@ -41,8 +39,6 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{ {
commandAssembly.GuardNotNull(nameof(commandAssembly));
var commandTypes = commandAssembly.ExportedTypes var commandTypes = commandAssembly.ExportedTypes
.Where(t => t.Implements(typeof(ICommand))) .Where(t => t.Implements(typeof(ICommand)))
.Where(t => t.IsDefined(typeof(CommandAttribute))) .Where(t => t.IsDefined(typeof(CommandAttribute)))
@@ -71,56 +67,56 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseTitle(string title) public ICliApplicationBuilder UseTitle(string title)
{ {
_title = title.GuardNotNull(nameof(title)); _title = title;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseExecutableName(string executableName) public ICliApplicationBuilder UseExecutableName(string executableName)
{ {
_executableName = executableName.GuardNotNull(nameof(executableName)); _executableName = executableName;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseVersionText(string versionText) public ICliApplicationBuilder UseVersionText(string versionText)
{ {
_versionText = versionText.GuardNotNull(nameof(versionText)); _versionText = versionText;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseDescription(string description) public ICliApplicationBuilder UseDescription(string? description)
{ {
_description = description; // can be null _description = description;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseConsole(IConsole console) public ICliApplicationBuilder UseConsole(IConsole console)
{ {
_console = console.GuardNotNull(nameof(console)); _console = console;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory) public ICliApplicationBuilder UseCommandFactory(ICommandFactory factory)
{ {
_commandFactory = factory.GuardNotNull(nameof(factory)); _commandFactory = factory;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter) public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
{ {
_commandOptionInputConverter = converter.GuardNotNull(nameof(converter)); _commandOptionInputConverter = converter;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider) public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider)
{ {
_environmentVariablesProvider = environmentVariablesProvider.GuardNotNull(nameof(environmentVariablesProvider)); _environmentVariablesProvider = environmentVariablesProvider;
return this; return this;
} }
@@ -128,13 +124,13 @@ namespace CliFx
public ICliApplication Build() public ICliApplication Build()
{ {
// Use defaults for required parameters that were not configured // Use defaults for required parameters that were not configured
_title = _title ?? GetDefaultTitle() ?? "App"; _title ??= GetDefaultTitle() ?? "App";
_executableName = _executableName ?? GetDefaultExecutableName() ?? "app"; _executableName ??= GetDefaultExecutableName() ?? "app";
_versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0"; _versionText ??= GetDefaultVersionText() ?? "v1.0";
_console = _console ?? new SystemConsole(); _console ??= new SystemConsole();
_commandFactory = _commandFactory ?? new CommandFactory(); _commandFactory ??= new CommandFactory();
_commandOptionInputConverter = _commandOptionInputConverter ?? new CommandOptionInputConverter(); _commandOptionInputConverter ??= new CommandOptionInputConverter();
_environmentVariablesProvider = _environmentVariablesProvider ?? new EnvironmentVariablesProvider(); _environmentVariablesProvider ??= new EnvironmentVariablesProvider();
// Project parameters to expected types // Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description); var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
@@ -153,7 +149,7 @@ namespace CliFx
// Entry assembly is null in tests // Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value; private static Assembly EntryAssembly => LazyEntryAssembly.Value;
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name; private static string GetDefaultTitle() => EntryAssembly?.GetName().Name ?? "";
private static string GetDefaultExecutableName() private static string GetDefaultExecutableName()
{ {
@@ -169,6 +165,6 @@ namespace CliFx
return Path.GetFileNameWithoutExtension(entryAssemblyLocation); return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
} }
private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : null; private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : "";
} }
} }

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks> <TargetFrameworks>net45;netstandard2.0;netstandard2.1</TargetFrameworks>
<Version>0.0.6</Version> <Version>0.0.8</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>
@@ -12,13 +12,23 @@
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
<PackageIcon>favicon.png</PackageIcon> <PackageIcon>favicon.png</PackageIcon>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile> <PublishRepositoryUrl>True</PublishRepositoryUrl>
<EmbedUntrackedSources>True</EmbedUntrackedSources>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.1.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" /> <None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup> </ItemGroup>

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
using CliFx.Services; using CliFx.Services;
@@ -17,9 +16,6 @@ namespace CliFx
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommands(this ICliApplicationBuilder builder, IReadOnlyList<Type> commandTypes) public static ICliApplicationBuilder AddCommands(this ICliApplicationBuilder builder, IReadOnlyList<Type> commandTypes)
{ {
builder.GuardNotNull(nameof(builder));
commandTypes.GuardNotNull(nameof(commandTypes));
foreach (var commandType in commandTypes) foreach (var commandType in commandTypes)
builder.AddCommand(commandType); builder.AddCommand(commandType);
@@ -31,9 +27,6 @@ namespace CliFx
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommandsFrom(this ICliApplicationBuilder builder, IReadOnlyList<Assembly> commandAssemblies) public static ICliApplicationBuilder AddCommandsFrom(this ICliApplicationBuilder builder, IReadOnlyList<Assembly> commandAssemblies)
{ {
builder.GuardNotNull(nameof(builder));
commandAssemblies.GuardNotNull(nameof(commandAssemblies));
foreach (var commandAssembly in commandAssemblies) foreach (var commandAssembly in commandAssemblies)
builder.AddCommandsFrom(commandAssembly); builder.AddCommandsFrom(commandAssembly);
@@ -43,21 +36,13 @@ namespace CliFx
/// <summary> /// <summary>
/// Adds commands from calling assembly to the application. /// Adds commands from calling assembly to the application.
/// </summary> /// </summary>
public static ICliApplicationBuilder AddCommandsFromThisAssembly(this ICliApplicationBuilder builder) public static ICliApplicationBuilder AddCommandsFromThisAssembly(this ICliApplicationBuilder builder) =>
{ builder.AddCommandsFrom(Assembly.GetCallingAssembly());
builder.GuardNotNull(nameof(builder));
return builder.AddCommandsFrom(Assembly.GetCallingAssembly());
}
/// <summary> /// <summary>
/// Configures application to use specified factory method for creating new instances of <see cref="ICommand"/>. /// Configures application to use specified factory method for creating new instances of <see cref="ICommand"/>.
/// </summary> /// </summary>
public static ICliApplicationBuilder UseCommandFactory(this ICliApplicationBuilder builder, Func<CommandSchema, ICommand> factoryMethod) public static ICliApplicationBuilder UseCommandFactory(this ICliApplicationBuilder builder, Func<CommandSchema, ICommand> factoryMethod) =>
{ builder.UseCommandFactory(new DelegateCommandFactory(factoryMethod));
builder.GuardNotNull(nameof(builder));
factoryMethod.GuardNotNull(nameof(factoryMethod));
return builder.UseCommandFactory(new DelegateCommandFactory(factoryMethod));
}
} }
} }

View File

@@ -47,7 +47,7 @@ namespace CliFx
/// <summary> /// <summary>
/// Sets application description, which appears in the help text. /// Sets application description, which appears in the help text.
/// </summary> /// </summary>
ICliApplicationBuilder UseDescription(string description); ICliApplicationBuilder UseDescription(string? description);
/// <summary> /// <summary>
/// Configures application to use specified implementation of <see cref="IConsole"/>. /// Configures application to use specified implementation of <see cref="IConsole"/>.

View File

@@ -1,5 +1,4 @@
using System.Threading; using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx.Services; using CliFx.Services;
namespace CliFx namespace CliFx
@@ -13,6 +12,6 @@ namespace CliFx
/// Executes command using specified implementation of <see cref="IConsole"/>. /// 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. /// This method is called when the command is invoked by a user through command line interface.
/// </summary> /// </summary>
Task ExecuteAsync(IConsole console, CancellationToken cancellationToken); Task ExecuteAsync(IConsole console);
} }
} }

View File

@@ -8,8 +8,6 @@ namespace CliFx.Internal
{ {
internal static class Extensions internal static class Extensions
{ {
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);
public static string Repeat(this char c, int count) => new string(c, count); 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 AsString(this char c) => c.Repeat(1);
@@ -36,9 +34,9 @@ namespace CliFx.Internal
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType); public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
public static Type GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type); public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
public static Type GetEnumerableUnderlyingType(this Type type) public static Type? GetEnumerableUnderlyingType(this Type type)
{ {
if (type.IsPrimitive) if (type.IsPrimitive)
return null; return null;
@@ -65,5 +63,8 @@ namespace CliFx.Internal
return array; return array;
} }
public static bool IsCollection(this Type type) =>
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
} }
} }

View File

@@ -1,13 +0,0 @@
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);
}
}

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using CliFx.Internal;
namespace CliFx.Models namespace CliFx.Models
{ {
@@ -30,7 +29,7 @@ namespace CliFx.Models
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes, public ApplicationConfiguration(IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed, bool isPreviewModeAllowed) bool isDebugModeAllowed, bool isPreviewModeAllowed)
{ {
CommandTypes = commandTypes.GuardNotNull(nameof(commandTypes)); CommandTypes = commandTypes;
IsDebugModeAllowed = isDebugModeAllowed; IsDebugModeAllowed = isDebugModeAllowed;
IsPreviewModeAllowed = isPreviewModeAllowed; IsPreviewModeAllowed = isPreviewModeAllowed;
} }

View File

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

View File

@@ -13,7 +13,7 @@ namespace CliFx.Models
/// Specified command name. /// Specified command name.
/// Can be null if command was not specified. /// Can be null if command was not specified.
/// </summary> /// </summary>
public string CommandName { get; } public string? CommandName { get; }
/// <summary> /// <summary>
/// Specified directives. /// Specified directives.
@@ -33,18 +33,19 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInput"/>. /// Initializes an instance of <see cref="CommandInput"/>.
/// </summary> /// </summary>
public CommandInput(string commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables) public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
IReadOnlyDictionary<string, string> environmentVariables)
{ {
CommandName = commandName; // can be null CommandName = commandName;
Directives = directives.GuardNotNull(nameof(directives)); Directives = directives;
Options = options.GuardNotNull(nameof(options)); Options = options;
EnvironmentVariables = environmentVariables.GuardNotNull(nameof(environmentVariables)); EnvironmentVariables = environmentVariables;
} }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInput"/>. /// Initializes an instance of <see cref="CommandInput"/>.
/// </summary> /// </summary>
public CommandInput(string commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options) public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(commandName, directives, options, EmptyEnvironmentVariables) : this(commandName, directives, options, EmptyEnvironmentVariables)
{ {
} }
@@ -52,7 +53,7 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInput"/>. /// Initializes an instance of <see cref="CommandInput"/>.
/// </summary> /// </summary>
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables) public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(commandName, EmptyDirectives, options, environmentVariables) : this(commandName, EmptyDirectives, options, environmentVariables)
{ {
} }
@@ -60,7 +61,7 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInput"/>. /// Initializes an instance of <see cref="CommandInput"/>.
/// </summary> /// </summary>
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options) public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
: this(commandName, EmptyDirectives, options) : this(commandName, EmptyDirectives, options)
{ {
} }
@@ -76,7 +77,7 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInput"/>. /// Initializes an instance of <see cref="CommandInput"/>.
/// </summary> /// </summary>
public CommandInput(string commandName) public CommandInput(string? commandName)
: this(commandName, EmptyOptions) : this(commandName, EmptyOptions)
{ {
} }
@@ -86,7 +87,7 @@ namespace CliFx.Models
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
if (!CommandName.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(CommandName))
buffer.Append(CommandName); buffer.Append(CommandName);
foreach (var directive in Directives) foreach (var directive in Directives)

View File

@@ -24,8 +24,8 @@ namespace CliFx.Models
/// </summary> /// </summary>
public CommandOptionInput(string alias, IReadOnlyList<string> values) public CommandOptionInput(string alias, IReadOnlyList<string> values)
{ {
Alias = alias.GuardNotNull(nameof(alias)); Alias = alias;
Values = values.GuardNotNull(nameof(values)); Values = values;
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using CliFx.Internal;
namespace CliFx.Models namespace CliFx.Models
{ {
@@ -12,12 +11,12 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Underlying property. /// Underlying property.
/// </summary> /// </summary>
public PropertyInfo Property { get; } public PropertyInfo? Property { get; }
/// <summary> /// <summary>
/// Option name. /// Option name.
/// </summary> /// </summary>
public string Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// Option short name. /// Option short name.
@@ -32,24 +31,24 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Option description. /// Option description.
/// </summary> /// </summary>
public string Description { get; } public string? Description { get; }
/// <summary> /// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified. /// Optional environment variable name that will be used as fallback value if no option value is specified.
/// </summary> /// </summary>
public string EnvironmentVariableName { get; } public string? EnvironmentVariableName { get; }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionSchema"/>. /// Initializes an instance of <see cref="CommandOptionSchema"/>.
/// </summary> /// </summary>
public CommandOptionSchema(PropertyInfo property, string name, char? shortName, bool isRequired, string description, string environmentVariableName) public CommandOptionSchema(PropertyInfo? property, string? name, char? shortName, bool isRequired, string? description, string? environmentVariableName)
{ {
Property = property; // can be null Property = property;
Name = name; // can be null Name = name;
ShortName = shortName; // can be null ShortName = shortName;
IsRequired = isRequired; IsRequired = isRequired;
Description = description; // can be null Description = description;
EnvironmentVariableName = environmentVariableName; //can be null EnvironmentVariableName = environmentVariableName;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -60,10 +59,10 @@ namespace CliFx.Models
if (IsRequired) if (IsRequired)
buffer.Append('*'); buffer.Append('*');
if (!Name.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name); buffer.Append(Name);
if (!Name.IsNullOrWhiteSpace() && ShortName != null) if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
buffer.Append('|'); buffer.Append('|');
if (ShortName != null) if (ShortName != null)

View File

@@ -13,17 +13,17 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Underlying type. /// Underlying type.
/// </summary> /// </summary>
public Type Type { get; } public Type? Type { get; }
/// <summary> /// <summary>
/// Command name. /// Command name.
/// </summary> /// </summary>
public string Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// Command description. /// Command description.
/// </summary> /// </summary>
public string Description { get; } public string? Description { get; }
/// <summary> /// <summary>
/// Command options. /// Command options.
@@ -33,12 +33,12 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandSchema"/>. /// Initializes an instance of <see cref="CommandSchema"/>.
/// </summary> /// </summary>
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandOptionSchema> options) public CommandSchema(Type? type, string? name, string? description, IReadOnlyList<CommandOptionSchema> options)
{ {
Type = type; // can be null Type = type;
Name = name; // can be null Name = name;
Description = description; // can be null Description = description;
Options = options.GuardNotNull(nameof(options)); Options = options;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -46,7 +46,7 @@ namespace CliFx.Models
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
if (!Name.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name); buffer.Append(Name);
foreach (var option in Options) foreach (var option in Options)

View File

@@ -13,13 +13,11 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Finds a command that has specified name, or null if not found. /// Finds a command that has specified name, or null if not found.
/// </summary> /// </summary>
public static CommandSchema FindByName(this IReadOnlyList<CommandSchema> commandSchemas, string commandName) public static CommandSchema? FindByName(this IReadOnlyList<CommandSchema> commandSchemas, string? commandName)
{ {
commandSchemas.GuardNotNull(nameof(commandSchemas));
// If looking for default command, don't compare names directly // If looking for default command, don't compare names directly
// ...because null and empty are both valid names for default command // ...because null and empty are both valid names for default command
if (commandName.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(commandName))
return commandSchemas.FirstOrDefault(c => c.IsDefault()); return commandSchemas.FirstOrDefault(c => c.IsDefault());
return commandSchemas.FirstOrDefault(c => string.Equals(c.Name, commandName, StringComparison.OrdinalIgnoreCase)); return commandSchemas.FirstOrDefault(c => string.Equals(c.Name, commandName, StringComparison.OrdinalIgnoreCase));
@@ -28,12 +26,10 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Finds parent command to the command that has specified name, or null if not found. /// Finds parent command to the command that has specified name, or null if not found.
/// </summary> /// </summary>
public static CommandSchema FindParent(this IReadOnlyList<CommandSchema> commandSchemas, string commandName) 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 command has no name, it's the default command so it doesn't have a parent
if (commandName.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(commandName))
return null; return null;
// Repeatedly cut off individual words from the name until we find a command with that name // Repeatedly cut off individual words from the name until we find a command with that name
@@ -56,12 +52,9 @@ namespace CliFx.Models
/// </summary> /// </summary>
public static bool MatchesAlias(this CommandOptionSchema optionSchema, string alias) public static bool MatchesAlias(this CommandOptionSchema optionSchema, string alias)
{ {
optionSchema.GuardNotNull(nameof(optionSchema));
alias.GuardNotNull(nameof(alias));
// Compare against name. Case is ignored. // Compare against name. Case is ignored.
var matchesByName = var matchesByName =
!optionSchema.Name.IsNullOrWhiteSpace() && !string.IsNullOrWhiteSpace(optionSchema.Name) &&
string.Equals(optionSchema.Name, alias, StringComparison.OrdinalIgnoreCase); string.Equals(optionSchema.Name, alias, StringComparison.OrdinalIgnoreCase);
// Compare against short name. Case is NOT ignored. // Compare against short name. Case is NOT ignored.
@@ -75,13 +68,8 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Finds an option input that matches the option schema specified, or null if not found. /// Finds an option input that matches the option schema specified, or null if not found.
/// </summary> /// </summary>
public static CommandOptionInput FindByOptionSchema(this IReadOnlyList<CommandOptionInput> optionInputs, CommandOptionSchema optionSchema) public static CommandOptionInput? FindByOptionSchema(this IReadOnlyList<CommandOptionInput> optionInputs, CommandOptionSchema optionSchema) =>
{ optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias));
optionInputs.GuardNotNull(nameof(optionInputs));
optionSchema.GuardNotNull(nameof(optionSchema));
return optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias));
}
/// <summary> /// <summary>
/// Gets valid aliases for the option. /// Gets valid aliases for the option.
@@ -90,8 +78,8 @@ namespace CliFx.Models
{ {
var result = new List<string>(2); var result = new List<string>(2);
if (!optionSchema.Name.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(optionSchema.Name))
result.Add(optionSchema.Name); result.Add(optionSchema.Name!);
if (optionSchema.ShortName != null) if (optionSchema.ShortName != null)
result.Add(optionSchema.ShortName.Value.AsString()); result.Add(optionSchema.ShortName.Value.AsString());
@@ -102,37 +90,25 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Gets whether a command was specified in the input. /// Gets whether a command was specified in the input.
/// </summary> /// </summary>
public static bool IsCommandSpecified(this CommandInput commandInput) public static bool IsCommandSpecified(this CommandInput commandInput) => !string.IsNullOrWhiteSpace(commandInput.CommandName);
{
commandInput.GuardNotNull(nameof(commandInput));
return !commandInput.CommandName.IsNullOrWhiteSpace();
}
/// <summary> /// <summary>
/// Gets whether debug directive was specified in the input. /// Gets whether debug directive was specified in the input.
/// </summary> /// </summary>
public static bool IsDebugDirectiveSpecified(this CommandInput commandInput) public static bool IsDebugDirectiveSpecified(this CommandInput commandInput) =>
{ commandInput.Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
commandInput.GuardNotNull(nameof(commandInput));
return commandInput.Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Gets whether preview directive was specified in the input. /// Gets whether preview directive was specified in the input.
/// </summary> /// </summary>
public static bool IsPreviewDirectiveSpecified(this CommandInput commandInput) public static bool IsPreviewDirectiveSpecified(this CommandInput commandInput) =>
{ commandInput.Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
commandInput.GuardNotNull(nameof(commandInput));
return commandInput.Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Gets whether help option was specified in the input. /// Gets whether help option was specified in the input.
/// </summary> /// </summary>
public static bool IsHelpOptionSpecified(this CommandInput commandInput) public static bool IsHelpOptionSpecified(this CommandInput commandInput)
{ {
commandInput.GuardNotNull(nameof(commandInput));
var firstOption = commandInput.Options.FirstOrDefault(); var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias); return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias);
} }
@@ -142,8 +118,6 @@ namespace CliFx.Models
/// </summary> /// </summary>
public static bool IsVersionOptionSpecified(this CommandInput commandInput) public static bool IsVersionOptionSpecified(this CommandInput commandInput)
{ {
commandInput.GuardNotNull(nameof(commandInput));
var firstOption = commandInput.Options.FirstOrDefault(); var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias); return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
} }
@@ -151,10 +125,6 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Gets whether this command is the default command, i.e. without a name. /// Gets whether this command is the default command, i.e. without a name.
/// </summary> /// </summary>
public static bool IsDefault(this CommandSchema commandSchema) public static bool IsDefault(this CommandSchema commandSchema) => string.IsNullOrWhiteSpace(commandSchema.Name);
{
commandSchema.GuardNotNull(nameof(commandSchema));
return commandSchema.Name.IsNullOrWhiteSpace();
}
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using CliFx.Internal;
namespace CliFx.Models namespace CliFx.Models
{ {
@@ -30,9 +29,9 @@ namespace CliFx.Models
IReadOnlyList<CommandSchema> availableCommandSchemas, IReadOnlyList<CommandSchema> availableCommandSchemas,
CommandSchema targetCommandSchema) CommandSchema targetCommandSchema)
{ {
ApplicationMetadata = applicationMetadata.GuardNotNull(nameof(applicationMetadata)); ApplicationMetadata = applicationMetadata;
AvailableCommandSchemas = availableCommandSchemas.GuardNotNull(nameof(availableCommandSchemas)); AvailableCommandSchemas = availableCommandSchemas;
TargetCommandSchema = targetCommandSchema.GuardNotNull(nameof(targetCommandSchema)); TargetCommandSchema = targetCommandSchema;
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
namespace CliFx.Services namespace CliFx.Services
@@ -10,10 +9,6 @@ namespace CliFx.Services
public class CommandFactory : ICommandFactory public class CommandFactory : ICommandFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
public ICommand CreateCommand(CommandSchema commandSchema) public ICommand CreateCommand(CommandSchema commandSchema) => (ICommand) Activator.CreateInstance(commandSchema.Type);
{
commandSchema.GuardNotNull(nameof(commandSchema));
return (ICommand) Activator.CreateInstance(commandSchema.Type);
}
} }
} }

View File

@@ -18,8 +18,8 @@ namespace CliFx.Services
/// </summary> /// </summary>
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser) public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
{ {
_commandOptionInputConverter = commandOptionInputConverter.GuardNotNull(nameof(commandOptionInputConverter)); _commandOptionInputConverter = commandOptionInputConverter;
_environmentVariablesParser = environmentVariablesParser.GuardNotNull(nameof(environmentVariablesParser)); _environmentVariablesParser = environmentVariablesParser;
} }
/// <summary> /// <summary>
@@ -41,29 +41,29 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput) 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 // Keep track of unset required options to report an error at a later stage
var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList(); var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList();
//Set command options //Set command options
foreach (var optionSchema in commandSchema.Options) foreach (var optionSchema in commandSchema.Options)
{ {
// Ignore special options that are not backed by a property
if (optionSchema.Property == null)
continue;
//Find matching option input //Find matching option input
var optionInput = commandInput.Options.FindByOptionSchema(optionSchema); var optionInput = commandInput.Options.FindByOptionSchema(optionSchema);
//If no option input is available fall back to environment variable values //If no option input is available fall back to environment variable values
if (optionInput == null && !optionSchema.EnvironmentVariableName.IsNullOrWhiteSpace()) if (optionInput == null && !string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
{ {
var fallbackEnvironmentVariableExists = commandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName); var fallbackEnvironmentVariableExists = commandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName!);
//If no environment variable is found or there is no valid value for this option skip it //If no environment variable is found or there is no valid value for this option skip it
if (!fallbackEnvironmentVariableExists || commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName].IsNullOrWhiteSpace()) if (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
continue; continue;
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName], optionSchema); optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
} }
//No fallback available and no option input was specified, skip option //No fallback available and no option input was specified, skip option

View File

@@ -19,8 +19,6 @@ namespace CliFx.Services
/// </summary> /// </summary>
public CommandInputParser(IEnvironmentVariablesProvider environmentVariablesProvider) public CommandInputParser(IEnvironmentVariablesProvider environmentVariablesProvider)
{ {
environmentVariablesProvider.GuardNotNull(nameof(environmentVariablesProvider));
_environmentVariablesProvider = environmentVariablesProvider; _environmentVariablesProvider = environmentVariablesProvider;
} }
@@ -35,8 +33,6 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments) public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
{ {
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
var commandNameBuilder = new StringBuilder(); var commandNameBuilder = new StringBuilder();
var directives = new List<string>(); var directives = new List<string>();
var optionsDic = new Dictionary<string, List<string>>(); var optionsDic = new Dictionary<string, List<string>>();
@@ -71,7 +67,7 @@ namespace CliFx.Services
} }
// Encountered directive or (part of) command name // Encountered directive or (part of) command name
else if (lastOptionAlias.IsNullOrWhiteSpace()) else if (string.IsNullOrWhiteSpace(lastOptionAlias))
{ {
if (commandLineArgument.StartsWith("[", StringComparison.OrdinalIgnoreCase) && if (commandLineArgument.StartsWith("[", StringComparison.OrdinalIgnoreCase) &&
commandLineArgument.EndsWith("]", StringComparison.OrdinalIgnoreCase)) commandLineArgument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
@@ -89,7 +85,7 @@ namespace CliFx.Services
} }
// Encountered option value // Encountered option value
else if (!lastOptionAlias.IsNullOrWhiteSpace()) else if (!string.IsNullOrWhiteSpace(lastOptionAlias))
{ {
optionsDic[lastOptionAlias].Add(commandLineArgument); optionsDic[lastOptionAlias].Add(commandLineArgument);
} }

View File

@@ -20,7 +20,7 @@ namespace CliFx.Services
/// </summary> /// </summary>
public CommandOptionInputConverter(IFormatProvider formatProvider) public CommandOptionInputConverter(IFormatProvider formatProvider)
{ {
_formatProvider = formatProvider.GuardNotNull(nameof(formatProvider)); _formatProvider = formatProvider;
} }
/// <summary> /// <summary>
@@ -34,10 +34,8 @@ namespace CliFx.Services
/// <summary> /// <summary>
/// Converts a single string value to specified target type. /// Converts a single string value to specified target type.
/// </summary> /// </summary>
protected virtual object ConvertValue(string value, Type targetType) protected virtual object? ConvertValue(string value, Type targetType)
{ {
targetType.GuardNotNull(nameof(targetType));
try try
{ {
// String or object // String or object
@@ -46,7 +44,7 @@ namespace CliFx.Services
// Bool // Bool
if (targetType == typeof(bool)) if (targetType == typeof(bool))
return value.IsNullOrWhiteSpace() || bool.Parse(value); return string.IsNullOrWhiteSpace(value) || bool.Parse(value);
// Char // Char
if (targetType == typeof(char)) if (targetType == typeof(char))
@@ -115,7 +113,7 @@ namespace CliFx.Services
// Nullable // Nullable
var nullableUnderlyingType = targetType.GetNullableUnderlyingType(); var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
if (nullableUnderlyingType != null) if (nullableUnderlyingType != null)
return !value.IsNullOrWhiteSpace() ? ConvertValue(value, nullableUnderlyingType) : null; return !string.IsNullOrWhiteSpace(value) ? ConvertValue(value, nullableUnderlyingType) : null;
// Has a constructor that accepts a single string // Has a constructor that accepts a single string
var stringConstructor = GetStringConstructor(targetType); var stringConstructor = GetStringConstructor(targetType);
@@ -143,11 +141,8 @@ namespace CliFx.Services
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual object ConvertOptionInput(CommandOptionInput optionInput, Type targetType) public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
{ {
optionInput.GuardNotNull(nameof(optionInput));
targetType.GuardNotNull(nameof(targetType));
// Get the underlying type of IEnumerable<T> if it's implemented by the target type. // Get the underlying type of IEnumerable<T> if it's implemented by the target type.
// Ignore string type because it's IEnumerable<T> but we don't treat it as such. // Ignore string type because it's IEnumerable<T> but we don't treat it as such.
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null; var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;

View File

@@ -36,7 +36,7 @@ namespace CliFx.Services
// Make sure there are no other options with the same name // Make sure there are no other options with the same name
var existingOptionWithSameName = result var existingOptionWithSameName = result
.Where(o => !o.Name.IsNullOrWhiteSpace()) .Where(o => !string.IsNullOrWhiteSpace(o.Name))
.FirstOrDefault(o => string.Equals(o.Name, optionSchema.Name, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(o => string.Equals(o.Name, optionSchema.Name, StringComparison.OrdinalIgnoreCase));
if (existingOptionWithSameName != null) if (existingOptionWithSameName != null)
@@ -68,8 +68,6 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes) public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
{ {
commandTypes.GuardNotNull(nameof(commandTypes));
// Make sure there's at least one command defined // Make sure there's at least one command defined
if (!commandTypes.Any()) if (!commandTypes.Any())
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
namespace CliFx.Services namespace CliFx.Services
@@ -16,14 +15,10 @@ namespace CliFx.Services
/// </summary> /// </summary>
public DelegateCommandFactory(Func<CommandSchema, ICommand> factoryMethod) public DelegateCommandFactory(Func<CommandSchema, ICommand> factoryMethod)
{ {
_factoryMethod = factoryMethod.GuardNotNull(nameof(factoryMethod)); _factoryMethod = factoryMethod;
} }
/// <inheritdoc /> /// <inheritdoc />
public ICommand CreateCommand(CommandSchema commandSchema) public ICommand CreateCommand(CommandSchema commandSchema) => _factoryMethod(commandSchema);
{
commandSchema.GuardNotNull(nameof(commandSchema));
return _factoryMethod(commandSchema);
}
} }
} }

View File

@@ -11,17 +11,14 @@ namespace CliFx.Services
/// <inheritdoct /> /// <inheritdoct />
public CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema) public CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema)
{ {
environmentVariableValue.GuardNotNull(nameof(environmentVariableValue));
targetOptionSchema.GuardNotNull(nameof(targetOptionSchema));
//If the option is not a collection do not split environment variable values //If the option is not a collection do not split environment variable values
var optionIsCollection = targetOptionSchema.Property.PropertyType.IsCollection(); var optionIsCollection = targetOptionSchema.Property != null && targetOptionSchema.Property.PropertyType.IsCollection();
if (!optionIsCollection) return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValue); if (!optionIsCollection) return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValue);
//If the option is a collection split the values using System separator, empty values are discarded //If the option is a collection split the values using System separator, empty values are discarded
var environmentVariableValues = environmentVariableValue.Split(Path.PathSeparator) var environmentVariableValues = environmentVariableValue.Split(Path.PathSeparator)
.Where(v => !v.IsNullOrWhiteSpace()) .Where(v => !string.IsNullOrWhiteSpace(v))
.ToList(); .ToList();
return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValues); return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValues);

View File

@@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using CliFx.Internal;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -14,9 +12,6 @@ namespace CliFx.Services
/// </summary> /// </summary>
public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action) public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
{ {
console.GuardNotNull(nameof(console));
action.GuardNotNull(nameof(action));
var lastColor = console.ForegroundColor; var lastColor = console.ForegroundColor;
console.ForegroundColor = foregroundColor; console.ForegroundColor = foregroundColor;
@@ -30,9 +25,6 @@ namespace CliFx.Services
/// </summary> /// </summary>
public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action) public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
{ {
console.GuardNotNull(nameof(console));
action.GuardNotNull(nameof(action));
var lastColor = console.BackgroundColor; var lastColor = console.BackgroundColor;
console.BackgroundColor = backgroundColor; console.BackgroundColor = backgroundColor;
@@ -44,32 +36,7 @@ namespace CliFx.Services
/// <summary> /// <summary>
/// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values. /// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
/// </summary> /// </summary>
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action) 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)); console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
}
/// <summary>
/// Gets wether a string representing an environment variable value is escaped (i.e.: surrounded by double quotation marks)
/// </summary>
public static bool IsEnvironmentVariableEscaped(this string environmentVariableValue)
{
environmentVariableValue.GuardNotNull(nameof(environmentVariableValue));
return environmentVariableValue.StartsWith("\"") && environmentVariableValue.EndsWith("\"");
}
/// <summary>
/// Gets wether the <see cref="Type"/> supplied is a collection implementing <see cref="IEnumerable{T}"/>
/// </summary>
public static bool IsCollection(this Type type)
{
type.GuardNotNull(nameof(type));
return type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
}
} }
} }

View File

@@ -14,9 +14,6 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public void RenderHelpText(IConsole console, HelpTextSource source) public void RenderHelpText(IConsole console, HelpTextSource source)
{ {
console.GuardNotNull(nameof(console));
source.GuardNotNull(nameof(source));
// Track position // Track position
var column = 0; var column = 0;
var row = 0; var row = 0;
@@ -105,7 +102,7 @@ namespace CliFx.Services
RenderNewLine(); RenderNewLine();
// Description // Description
if (!source.ApplicationMetadata.Description.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(source.ApplicationMetadata.Description))
{ {
Render(source.ApplicationMetadata.Description); Render(source.ApplicationMetadata.Description);
RenderNewLine(); RenderNewLine();
@@ -114,7 +111,7 @@ namespace CliFx.Services
void RenderDescription() void RenderDescription()
{ {
if (source.TargetCommandSchema.Description.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(source.TargetCommandSchema.Description))
return; return;
// Margin // Margin
@@ -142,7 +139,7 @@ namespace CliFx.Services
Render(source.ApplicationMetadata.ExecutableName); Render(source.ApplicationMetadata.ExecutableName);
// Command name // Command name
if (!source.TargetCommandSchema.IsDefault()) if (!string.IsNullOrWhiteSpace(source.TargetCommandSchema.Name))
{ {
Render(" "); Render(" ");
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan); RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
@@ -195,19 +192,19 @@ namespace CliFx.Services
} }
// Delimiter // Delimiter
if (!optionSchema.Name.IsNullOrWhiteSpace() && optionSchema.ShortName != null) if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName != null)
{ {
Render("|"); Render("|");
} }
// Name // Name
if (!optionSchema.Name.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(optionSchema.Name))
{ {
RenderWithColor($"--{optionSchema.Name}", ConsoleColor.White); RenderWithColor($"--{optionSchema.Name}", ConsoleColor.White);
} }
// Description // Description
if (!optionSchema.Description.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(optionSchema.Description))
{ {
RenderColumnIndent(); RenderColumnIndent();
Render(optionSchema.Description); Render(optionSchema.Description);
@@ -231,14 +228,14 @@ namespace CliFx.Services
// Child commands // Child commands
foreach (var childCommandSchema in childCommandSchemas) foreach (var childCommandSchema in childCommandSchemas)
{ {
var relativeCommandName = GetRelativeCommandName(childCommandSchema, source.TargetCommandSchema); var relativeCommandName = GetRelativeCommandName(childCommandSchema, source.TargetCommandSchema)!;
// Name // Name
RenderIndent(); RenderIndent();
RenderWithColor(relativeCommandName, ConsoleColor.Cyan); RenderWithColor(relativeCommandName, ConsoleColor.Cyan);
// Description // Description
if (!childCommandSchema.Description.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
{ {
RenderColumnIndent(); RenderColumnIndent();
Render(childCommandSchema.Description); Render(childCommandSchema.Description);
@@ -254,7 +251,7 @@ namespace CliFx.Services
Render("You can run `"); Render("You can run `");
Render(source.ApplicationMetadata.ExecutableName); Render(source.ApplicationMetadata.ExecutableName);
if (!source.TargetCommandSchema.IsDefault()) if (!string.IsNullOrWhiteSpace(source.TargetCommandSchema.Name))
{ {
Render(" "); Render(" ");
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan); RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
@@ -285,8 +282,8 @@ namespace CliFx.Services
public partial class HelpTextRenderer public partial class HelpTextRenderer
{ {
private static string GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) => private static string? GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) =>
parentCommandSchema.Name.IsNullOrWhiteSpace() string.IsNullOrWhiteSpace(parentCommandSchema.Name) || string.IsNullOrWhiteSpace(commandSchema.Name)
? commandSchema.Name ? commandSchema.Name
: commandSchema.Name.Substring(parentCommandSchema.Name.Length + 1); : commandSchema.Name.Substring(parentCommandSchema.Name.Length + 1);
} }

View File

@@ -11,6 +11,6 @@ namespace CliFx.Services
/// <summary> /// <summary>
/// Converts an option to specified target type. /// Converts an option to specified target type.
/// </summary> /// </summary>
object ConvertOptionInput(CommandOptionInput optionInput, Type targetType); object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType);
} }
} }

View File

@@ -55,8 +55,9 @@ namespace CliFx.Services
void ResetColor(); void ResetColor();
/// <summary> /// <summary>
/// Cancels when soft cancellation requested. /// Provides token that cancels when application cancellation is requested.
/// Subsequent calls return the same token.
/// </summary> /// </summary>
CancellationToken CancellationToken { get; } CancellationToken GetCancellationToken();
} }
} }

View File

@@ -9,21 +9,7 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class SystemConsole : IConsole public class SystemConsole : IConsole
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource? _cancellationTokenSource;
/// <inheritdoc />
public SystemConsole()
{
// Subscribe to CancelKeyPress event with cancellation token source
// Kills app on second cancellation (hard cancellation)
Console.CancelKeyPress += (_, args) =>
{
if (_cancellationTokenSource.IsCancellationRequested)
return;
args.Cancel = true;
_cancellationTokenSource.Cancel();
};
}
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input => Console.In; public TextReader Input => Console.In;
@@ -61,6 +47,24 @@ namespace CliFx.Services
public void ResetColor() => Console.ResetColor(); public void ResetColor() => Console.ResetColor();
/// <inheritdoc /> /// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token; public CancellationToken GetCancellationToken()
{
if (_cancellationTokenSource is null)
{
_cancellationTokenSource = new CancellationTokenSource();
// Subscribe to CancelKeyPress event with cancellation token source
// Kills app on second cancellation (hard cancellation)
Console.CancelKeyPress += (_, args) =>
{
if (_cancellationTokenSource.IsCancellationRequested)
return;
args.Cancel = true;
_cancellationTokenSource.Cancel();
};
}
return _cancellationTokenSource.Token;
}
} }
} }

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using CliFx.Internal;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -12,7 +11,7 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class VirtualConsole : IConsole public class VirtualConsole : IConsole
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly CancellationToken _cancellationToken;
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input { get; } public TextReader Input { get; }
@@ -43,21 +42,24 @@ namespace CliFx.Services
/// </summary> /// </summary>
public VirtualConsole(TextReader input, bool isInputRedirected, public VirtualConsole(TextReader input, bool isInputRedirected,
TextWriter output, bool isOutputRedirected, TextWriter output, bool isOutputRedirected,
TextWriter error, bool isErrorRedirected) TextWriter error, bool isErrorRedirected,
CancellationToken cancellationToken = default)
{ {
Input = input.GuardNotNull(nameof(input)); Input = input;
IsInputRedirected = isInputRedirected; IsInputRedirected = isInputRedirected;
Output = output.GuardNotNull(nameof(output)); Output = output;
IsOutputRedirected = isOutputRedirected; IsOutputRedirected = isOutputRedirected;
Error = error.GuardNotNull(nameof(error)); Error = error;
IsErrorRedirected = isErrorRedirected; IsErrorRedirected = isErrorRedirected;
_cancellationToken = cancellationToken;
} }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>. /// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary> /// </summary>
public VirtualConsole(TextReader input, TextWriter output, TextWriter error) public VirtualConsole(TextReader input, TextWriter output, TextWriter error,
: this(input, true, output, true, error, true) CancellationToken cancellationToken = default)
: this(input, true, output, true, error, true, cancellationToken)
{ {
} }
@@ -65,8 +67,8 @@ namespace CliFx.Services
/// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout) and error stream (stderr). /// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout) and error stream (stderr).
/// Input stream (stdin) is replaced with a no-op stub. /// Input stream (stdin) is replaced with a no-op stub.
/// </summary> /// </summary>
public VirtualConsole(TextWriter output, TextWriter error) public VirtualConsole(TextWriter output, TextWriter error, CancellationToken cancellationToken = default)
: this(TextReader.Null, output, error) : this(TextReader.Null, output, error, cancellationToken)
{ {
} }
@@ -74,8 +76,8 @@ namespace CliFx.Services
/// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout). /// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout).
/// Input stream (stdin) and error stream (stderr) are replaced with no-op stubs. /// Input stream (stdin) and error stream (stderr) are replaced with no-op stubs.
/// </summary> /// </summary>
public VirtualConsole(TextWriter output) public VirtualConsole(TextWriter output, CancellationToken cancellationToken = default)
: this(output, TextWriter.Null) : this(output, TextWriter.Null, cancellationToken)
{ {
} }
@@ -87,14 +89,6 @@ namespace CliFx.Services
} }
/// <inheritdoc /> /// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token; public CancellationToken GetCancellationToken() => _cancellationToken;
/// <summary>
/// Simulates cancellation.
/// </summary>
public void Cancel()
{
_cancellationTokenSource.Cancel();
}
} }
} }

View File

@@ -1,10 +1,9 @@
# CliFx # CliFx
[![Build](https://img.shields.io/appveyor/ci/Tyrrrz/CliFx/master.svg)](https://ci.appveyor.com/project/Tyrrrz/CliFx/branch/master) [![Build](https://github.com/Tyrrrz/CliFx/workflows/CI/badge.svg?branch=master)](https://github.com/Tyrrrz/CliFx/actions)
[![Tests](https://img.shields.io/appveyor/tests/Tyrrrz/CliFx/master.svg)](https://ci.appveyor.com/project/Tyrrrz/CliFx/branch/master/tests) [![Coverage](https://codecov.io/gh/Tyrrrz/CliFx/branch/master/graph/badge.svg)](https://codecov.io/gh/Tyrrrz/CliFx)
[![Coverage](https://img.shields.io/codecov/c/gh/Tyrrrz/CliFx/master.svg)](https://codecov.io/gh/Tyrrrz/CliFx) [![Version](https://img.shields.io/nuget/v/CliFx.svg)](https://nuget.org/packages/CliFx)
[![NuGet](https://img.shields.io/nuget/v/CliFx.svg)](https://nuget.org/packages/CliFx) [![Downloads](https://img.shields.io/nuget/dt/CliFx.svg)](https://nuget.org/packages/CliFx)
[![NuGet](https://img.shields.io/nuget/dt/CliFx.svg)](https://nuget.org/packages/CliFx)
[![Donate](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://patreon.com/tyrrrz) [![Donate](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://patreon.com/tyrrrz)
[![Donate](https://img.shields.io/badge/buymeacoffee-donate-yellow.svg)](https://buymeacoffee.com/tyrrrz) [![Donate](https://img.shields.io/badge/buymeacoffee-donate-yellow.svg)](https://buymeacoffee.com/tyrrrz)
@@ -15,7 +14,6 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
## Download ## Download
- [NuGet](https://nuget.org/packages/CliFx): `dotnet add package CliFx` - [NuGet](https://nuget.org/packages/CliFx): `dotnet add package CliFx`
- [Continuous integration](https://ci.appveyor.com/project/Tyrrrz/CliFx)
## Features ## Features
@@ -24,6 +22,7 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
- Resolves commands and options using attributes - Resolves commands and options using attributes
- Handles options of various types, including custom types - Handles options of various types, including custom types
- Supports multi-level command hierarchies - Supports multi-level command hierarchies
- Allows cancellation
- Generates contextual help text - Generates contextual help text
- Prints errors and routes exit codes on exceptions - Prints errors and routes exit codes on exceptions
- Highly testable and easy to debug - Highly testable and easy to debug
@@ -99,7 +98,7 @@ public class LogCommand : ICommand
By implementing `ICommand` this class also provides `ExecuteAsync` method. This is the method that gets called when the user invokes the command. Its return type is `Task` in order to facilitate asynchronous execution, but if your command runs synchronously you can simply return `Task.CompletedTask`. By implementing `ICommand` this class also provides `ExecuteAsync` method. This is the method that gets called when the user invokes the command. Its return type is `Task` in order to facilitate asynchronous execution, but if your command runs synchronously you can simply return `Task.CompletedTask`.
The `ExecuteAsync` method also takes an instance of `IConsole` as a parameter. You should use this abstraction to interact with the console instead of calling `System.Console` so that your commands are testable. The `ExecuteAsync` method also takes an instance of `IConsole` as a parameter. You should use the `console` parameter in places where you would normally use `System.Console`, in order to make your command testable.
Finally, the command defined above can be executed from the command line in one of the following ways: Finally, the command defined above can be executed from the command line in one of the following ways:
@@ -215,6 +214,30 @@ public class SecondSubCommand : ICommand
} }
``` ```
### Cancellation
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break). You can call `console.GetCancellationToken()` to override the default behavior and get `CancellationToken` that represents the first interrupt signal. Second interrupt signal terminates an app immediately. Note that the code that executes before the first call to `GetCancellationToken` will not be cancellation aware.
You can pass `CancellationToken` around and check its state.
Cancelled or terminated app returns non-zero exit code.
```c#
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async Task ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Printed");
// Long-running cancellable operation that throws when canceled
await Task.Delay(Timeout.InfiniteTimeSpan, console.GetCancellationToken());
console.Output.WriteLine("Never printed");
}
}
```
### Dependency injection ### Dependency injection
CliFx uses an implementation of `ICommandFactory` to initialize commands and by default it only works with types that have parameterless constructors. CliFx uses an implementation of `ICommandFactory` to initialize commands and by default it only works with types that have parameterless constructors.
@@ -476,4 +499,4 @@ CliFx is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework"
## Donate ## Donate
If you really like my projects and want to support me, consider donating to me on [Patreon](https://patreon.com/tyrrrz) or [BuyMeACoffee](https://buymeacoffee.com/tyrrrz). All donations are optional and are greatly appreciated. 🙏 If you really like my projects and want to support me, consider donating to me on [Patreon](https://patreon.com/tyrrrz) or [BuyMeACoffee](https://buymeacoffee.com/tyrrrz). All donations are optional and are greatly appreciated. 🙏

View File

@@ -1,26 +0,0 @@
version: '{build}'
image: Visual Studio 2019
configuration: Release
before_build:
- dotnet restore
build:
verbosity: minimal
after_test:
- choco install codecov && codecov -f "CliFx.Tests/bin/%CONFIGURATION%/Coverage.xml" --required
artifacts:
- path: CliFx/bin/$(configuration)/CliFx*.nupkg
name: CliFx.nupkg
deploy:
- provider: NuGet
api_key:
secure: 5VyEaGo5gRLr9HdkRFqS1enRq+K8Qarg1dzU33CE1dOmVXp43JaS2PQTNgsRHXkc
artifact: CliFx.nupkg
on:
branch: master
appveyor_repo_tag: true