20 Commits
0.0.4 ... 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
Alexey Golub
9564cd5d30 Update version 2019-10-30 18:41:24 +02:00
Moophic
ed458c3980 Cancellation support (#28) 2019-10-30 18:37:32 +02:00
Alexey Golub
25538f99db Migrate from PackageIconUrl to PackageIcon 2019-10-08 16:59:13 +03:00
Federico Paolillo
36436e7a4b Environment variables (#27) 2019-09-29 20:44:24 +03:00
Alexey Golub
a6070332c9 Migrate to .NET Core 3 where applicable 2019-09-25 22:52:33 +03:00
Alexey Golub
25cbfdb4b8 Move screenshots to repository 2019-09-06 20:24:28 +03:00
Alexey Golub
d1b5107c2c Update version 2019-08-26 20:48:43 +03:00
Alexey Golub
03873d63cd Fix exception when converting option values to array when there's only one value 2019-08-26 20:47:23 +03:00
Alexey Golub
89aba39964 Add extensibility point for injecting custom option converters
Closes #19
2019-08-26 20:10:37 +03:00
Alexey Golub
ab57a103d1 Update benchmarks 2019-08-26 17:20:14 +03:00
Alexey Golub
d0b2ebc061 Update readme 2019-08-25 23:27:19 +03:00
76 changed files with 1068 additions and 561 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

1
.gitignore vendored
View File

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

BIN
.screenshots/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -8,7 +8,7 @@ namespace CliFx.Benchmarks
[RankColumn] [RankColumn]
public class Benchmark public class Benchmark
{ {
private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[Benchmark(Description = "CliFx", Baseline = true)] [Benchmark(Description = "CliFx", Baseline = true)]
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
@@ -19,16 +19,17 @@ namespace CliFx.Benchmarks
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments); public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
// Skipped because this benchmark freezes after a couple of iterations [Benchmark(Description = "CommandLineParser")]
// Probably wasn't designed to run multiple times in single process execution
//[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser() public void ExecuteWithCommandLineParser()
{ {
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand)); var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand));
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute()); CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
} }
[Benchmark(Description = "PowerArgs")] [Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments); public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
[Benchmark(Description = "Clipr")]
public void ExecuteWithClipr() => clipr.CliParser.Parse<CliprCommand>(Arguments).Execute();
} }
} }

View File

@@ -2,12 +2,13 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> <PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
<PackageReference Include="PowerArgs" Version="3.6.0" /> <PackageReference Include="PowerArgs" Version="3.6.0" />

View File

@@ -8,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; }

View File

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

View File

@@ -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

@@ -2,8 +2,8 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -24,7 +24,7 @@ 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)
{ {

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

@@ -1,8 +1,9 @@
using System; using NUnit.Framework;
using System;
using System.IO; using System.IO;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
using NUnit.Framework;
namespace CliFx.Tests namespace CliFx.Tests
{ {
@@ -30,7 +31,9 @@ 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())
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
.Build(); .Build();
} }

View File

@@ -1,11 +1,13 @@
using System; using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests namespace CliFx.Tests
{ {
@@ -21,13 +23,13 @@ namespace CliFx.Tests
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", ", "},
@@ -51,7 +53,7 @@ namespace CliFx.Tests
new[] {"--version"}, new[] {"--version"},
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] {typeof(HelloWorldDefaultCommand)},
new[] {"-h"}, new[] {"-h"},
@@ -63,13 +65,13 @@ namespace CliFx.Tests
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"},
@@ -81,7 +83,7 @@ namespace CliFx.Tests
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"},
@@ -150,13 +152,13 @@ namespace CliFx.Tests
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"},
@@ -167,64 +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 application = new CliApplicationBuilder() var console = new VirtualConsole(stdoutStream);
.AddCommands(commandTypes) var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
.UseVersionText(TestVersionText)
.UseConsole(console)
.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 application = new CliApplicationBuilder() var console = new VirtualConsole(TextWriter.Null, stderrStream);
.AddCommands(commandTypes) var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
.UseVersionText(TestVersionText)
.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); // Assert
if (expectedExitCode != null)
if (expectedStdErr != null) exitCode.Should().Be(expectedExitCode);
stderr.Should().Be(expectedStdErr); else
else exitCode.Should().NotBe(0);
stderr.Should().NotBeNullOrWhiteSpace();
} if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
else
stderr.Should().NotBeNullOrWhiteSpace();
}
[Test]
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using var cancellationTokenSource = new CancellationTokenSource();
await using var stdoutStream = new StringWriter();
var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.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,24 +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>
<LangVersion>latest</LangVersion> <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

@@ -1,12 +1,14 @@
using System; using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Models; using CliFx.Models;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
using FluentAssertions; using CliFx.Tests.Stubs;
using NUnit.Framework; using System.IO;
namespace CliFx.Tests.Services namespace CliFx.Tests.Services
{ {
@@ -14,7 +16,7 @@ namespace CliFx.Tests.Services
public class CommandInitializerTests public class CommandInitializerTests
{ {
private static CommandSchema GetCommandSchema(Type commandType) => private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single(); new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single();
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand() private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
{ {
@@ -26,7 +28,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("dividend", "13"), new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8") new CommandOptionInput("divisor", "8")
}), }),
new DivideCommand {Dividend = 13, Divisor = 8} new DivideCommand { Dividend = 13, Divisor = 8 }
); );
yield return new TestCaseData( yield return new TestCaseData(
@@ -37,7 +39,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("dividend", "13"), new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8") new CommandOptionInput("d", "8")
}), }),
new DivideCommand {Dividend = 13, Divisor = 8} new DivideCommand { Dividend = 13, Divisor = 8 }
); );
yield return new TestCaseData( yield return new TestCaseData(
@@ -48,7 +50,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("D", "13"), new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8") new CommandOptionInput("d", "8")
}), }),
new DivideCommand {Dividend = 13, Divisor = 8} new DivideCommand { Dividend = 13, Divisor = 8 }
); );
yield return new TestCaseData( yield return new TestCaseData(
@@ -58,7 +60,7 @@ namespace CliFx.Tests.Services
{ {
new CommandOptionInput("i", new[] {"foo", " ", "bar"}) new CommandOptionInput("i", new[] {"foo", " ", "bar"})
}), }),
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}} new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
); );
yield return new TestCaseData( yield return new TestCaseData(
@@ -69,7 +71,43 @@ namespace CliFx.Tests.Services
new CommandOptionInput("i", new[] {"foo", "bar"}), new CommandOptionInput("i", new[] {"foo", "bar"}),
new CommandOptionInput("s", " ") new CommandOptionInput("s", " ")
}), }),
new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "} new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " }
);
//Will read a value from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableCommand { Option = "A" }
);
//Will read multiple values from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableWithMultipleValuesCommand(),
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } }
);
//Will not read a value from environment variables because one is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, new[]
{
new CommandOptionInput("opt", new[] { "X" })
},
EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableCommand { Option = "X" }
);
//Will not split environment variable values because underlying property is not a collection
yield return new TestCaseData(
new EnvironmentVariableWithoutCollectionPropertyCommand(),
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
); );
} }

View File

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

View File

@@ -214,6 +214,12 @@ namespace CliFx.Tests.Services
new[] {47, 69} new[] {47, 69}
); );
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"47"}),
typeof(int[]),
new[] {47}
);
yield return new TestCaseData( yield return new TestCaseData(
new CommandOptionInput("option", new[] {"value1", "value3"}), new CommandOptionInput("option", new[] {"value1", "value3"}),
typeof(TestEnum[]), typeof(TestEnum[]),
@@ -270,6 +276,16 @@ namespace CliFx.Tests.Services
typeof(int) typeof(int)
); );
yield return new TestCaseData(
new CommandOptionInput("option", new[] {"123", "456"}),
typeof(int)
);
yield return new TestCaseData(
new CommandOptionInput("option"),
typeof(int)
);
yield return new TestCaseData( yield return new TestCaseData(
new CommandOptionInput("option", "123"), new CommandOptionInput("option", "123"),
typeof(TestNonStringParseable) typeof(TestNonStringParseable)

View File

@@ -1,11 +1,11 @@
using System; using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Models; using CliFx.Models;
using CliFx.Services; using CliFx.Services;
using CliFx.Tests.TestCommands; using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services namespace CliFx.Tests.Services
{ {
@@ -15,30 +15,37 @@ namespace CliFx.Tests.Services
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas() private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DivideCommand), typeof(ConcatCommand)}, new[] { typeof(DivideCommand), typeof(ConcatCommand), typeof(EnvironmentVariableCommand) },
new[] new[]
{ {
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.", new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new[] new[]
{ {
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)), new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
"dividend", 'D', true, "The number to divide."), "dividend", 'D', true, "The number to divide.", null),
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)), new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
"divisor", 'd', true, "The number to divide by.") "divisor", 'd', true, "The number to divide by.", null)
}), }),
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.", new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
new[] new[]
{ {
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)), new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
null, 'i', true, "Input strings."), null, 'i', true, "Input strings.", null),
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)), new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
null, 's', false, "String separator.") null, 's', false, "String separator.", null)
}) }),
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
new[]
{
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
"opt", null, false, null, "ENV_SINGLE_VALUE")
}
)
} }
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] { typeof(HelloWorldDefaultCommand) },
new[] new[]
{ {
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0]) new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])
@@ -62,7 +69,7 @@ namespace CliFx.Tests.Services
{ {
new[] {typeof(NonAnnotatedCommand)} new[] {typeof(NonAnnotatedCommand)}
}); });
yield return new TestCaseData(new object[] yield return new TestCaseData(new object[]
{ {
new[] {typeof(DuplicateOptionNamesCommand)} new[] {typeof(DuplicateOptionNamesCommand)}
@@ -72,7 +79,7 @@ namespace CliFx.Tests.Services
{ {
new[] {typeof(DuplicateOptionShortNamesCommand)} new[] {typeof(DuplicateOptionShortNamesCommand)}
}); });
yield return new TestCaseData(new object[] yield return new TestCaseData(new object[]
{ {
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)} new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}

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

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ 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) => throw new CommandException(Message, ExitCode); public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
} }

View File

@@ -8,10 +8,10 @@ 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) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }

View File

@@ -8,10 +8,10 @@ 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) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ 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) => throw new Exception(Message); public Task ExecuteAsync(IConsole console) => throw new Exception(Message);
} }

View File

@@ -8,10 +8,10 @@ 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) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }

View File

@@ -8,10 +8,10 @@ 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) => Task.CompletedTask; public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
} }

View File

@@ -8,7 +8,7 @@ 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) => 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,15 +28,20 @@ namespace CliFx.Attributes
/// <summary> /// <summary>
/// Option description, which is used in help text. /// Option description, which is used in help text.
/// </summary> /// </summary>
public string Description { get; set; } public string? Description { get; set; }
/// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified.
/// </summary>
public string? EnvironmentVariableName { get; set; }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(string name, char? shortName) private CommandOptionAttribute(string? name, char? shortName)
{ {
Name = name; // can be null Name = name;
ShortName = shortName; // can be null ShortName = shortName;
} }
/// <summary> /// <summary>
@@ -57,7 +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;
@@ -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,18 +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 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;
@@ -39,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)))
@@ -69,42 +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;
}
/// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
{
_commandOptionInputConverter = converter;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider)
{
_environmentVariablesProvider = environmentVariablesProvider;
return this; return this;
} }
@@ -112,19 +124,21 @@ 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 ??= new CommandOptionInputConverter();
_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);
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed); var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration, return new CliApplication(metadata, configuration,
_console, new CommandInputParser(), new CommandSchemaResolver(), _console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(), new HelpTextRenderer()); _commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
} }
} }
@@ -135,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()
{ {
@@ -151,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,9 +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>
<LangVersion>latest</LangVersion> <Version>0.0.8</Version>
<Version>0.0.4</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>
@@ -11,13 +10,27 @@
<PackageTags>command line executable interface framework parser arguments net core</PackageTags> <PackageTags>command line executable interface framework parser arguments net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl> <PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
<PackageIconUrl>https://raw.githubusercontent.com/Tyrrrz/CliFx/master/favicon.png</PackageIconUrl> <PackageIcon>favicon.png</PackageIcon>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>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>
</Project> <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>
<None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup>
</Project>

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"/>.
@@ -59,6 +59,16 @@ namespace CliFx
/// </summary> /// </summary>
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory); ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
/// <summary>
/// Configures application to use specified implementation of <see cref="ICommandOptionInputConverter"/>.
/// </summary>
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter);
/// <summary>
/// Configures application to use specified implementation of <see cref="IEnvironmentVariablesProvider"/>.
/// </summary>
ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider);
/// <summary> /// <summary>
/// Creates an instance of <see cref="ICliApplication"/> using configured parameters. /// Creates an instance of <see cref="ICliApplication"/> using configured parameters.
/// Default values are used in place of parameters that were not specified. /// Default values are used in place of parameters that were not specified.

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,8 +34,13 @@ 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 GetEnumerableUnderlyingType(this Type type) public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
public static Type? GetEnumerableUnderlyingType(this Type type)
{ {
if (type.IsPrimitive)
return null;
if (type == typeof(IEnumerable)) if (type == typeof(IEnumerable))
return typeof(object); return typeof(object);
@@ -60,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.
@@ -25,20 +25,43 @@ namespace CliFx.Models
/// </summary> /// </summary>
public IReadOnlyList<CommandOptionInput> Options { get; } public IReadOnlyList<CommandOptionInput> Options { get; }
/// <summary>
/// Environment variables available when the command was parsed
/// </summary>
public IReadOnlyDictionary<string, string> EnvironmentVariables { get; }
/// <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,
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;
} }
/// <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<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(commandName, directives, options, EmptyEnvironmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(commandName, EmptyDirectives, options, environmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
: this(commandName, EmptyDirectives, options) : this(commandName, EmptyDirectives, options)
{ {
} }
@@ -54,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)
{ {
} }
@@ -64,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)
@@ -87,6 +110,7 @@ namespace CliFx.Models
{ {
private static readonly IReadOnlyList<string> EmptyDirectives = new string[0]; private static readonly IReadOnlyList<string> EmptyDirectives = new string[0];
private static readonly IReadOnlyList<CommandOptionInput> EmptyOptions = new CommandOptionInput[0]; private static readonly IReadOnlyList<CommandOptionInput> EmptyOptions = new CommandOptionInput[0];
private static readonly IReadOnlyDictionary<string, string> EmptyEnvironmentVariables = new Dictionary<string, string>();
/// <summary> /// <summary>
/// Empty input. /// Empty input.

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,18 +31,24 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Option description. /// Option description.
/// </summary> /// </summary>
public string Description { get; } public string? Description { get; }
/// <summary>
/// Optional environment variable name that will be used as fallback value if no option value is specified.
/// </summary>
public string? EnvironmentVariableName { get; }
/// <summary> /// <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) 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;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -54,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)
@@ -75,9 +80,9 @@ namespace CliFx.Models
// ...in CliApplication (when reading) and HelpTextRenderer (when writing). // ...in CliApplication (when reading) and HelpTextRenderer (when writing).
internal static CommandOptionSchema HelpOption { get; } = internal static CommandOptionSchema HelpOption { get; } =
new CommandOptionSchema(null, "help", 'h', false, "Shows help text."); new CommandOptionSchema(null, "help", 'h', false, "Shows help text.", null);
internal static CommandOptionSchema VersionOption { get; } = internal static CommandOptionSchema VersionOption { get; } =
new CommandOptionSchema(null, "version", null, false, "Shows version information."); new CommandOptionSchema(null, "version", null, false, "Shows version information.", 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

@@ -1,7 +1,7 @@
using System; using CliFx.Internal;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using CliFx.Internal;
namespace CliFx.Models namespace CliFx.Models
{ {
@@ -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.
@@ -71,17 +64,12 @@ namespace CliFx.Models
return matchesByName || matchesByShortName; return matchesByName || matchesByShortName;
} }
/// <summary>
/// Finds an option that matches specified alias, or null if not found.
/// </summary>
public static CommandOptionSchema FindByAlias(this IReadOnlyList<CommandOptionSchema> optionSchemas, string alias)
{
optionSchemas.GuardNotNull(nameof(optionSchemas));
alias.GuardNotNull(nameof(alias));
return optionSchemas.FirstOrDefault(o => o.MatchesAlias(alias)); /// <summary>
} /// Finds an option input that matches the option schema specified, or null if not found.
/// </summary>
public static CommandOptionInput? FindByOptionSchema(this IReadOnlyList<CommandOptionInput> optionInputs, CommandOptionSchema optionSchema) =>
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

@@ -11,42 +11,65 @@ namespace CliFx.Services
public class CommandInitializer : ICommandInitializer public class CommandInitializer : ICommandInitializer
{ {
private readonly ICommandOptionInputConverter _commandOptionInputConverter; private readonly ICommandOptionInputConverter _commandOptionInputConverter;
private readonly IEnvironmentVariablesParser _environmentVariablesParser;
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>. /// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary> /// </summary>
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter) public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
{
_commandOptionInputConverter = commandOptionInputConverter;
_environmentVariablesParser = environmentVariablesParser;
}
/// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser)
: this(new CommandOptionInputConverter(), environmentVariablesParser)
{ {
_commandOptionInputConverter = commandOptionInputConverter.GuardNotNull(nameof(commandOptionInputConverter));
} }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>. /// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary> /// </summary>
public CommandInitializer() public CommandInitializer()
: this(new CommandOptionInputConverter()) : this(new CommandOptionInputConverter(), new EnvironmentVariablesParser())
{ {
} }
/// <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 optionInput in commandInput.Options) foreach (var optionSchema in commandSchema.Options)
{ {
// Find matching option schema for this option input // Ignore special options that are not backed by a property
var optionSchema = commandSchema.Options.FindByAlias(optionInput.Alias); if (optionSchema.Property == null)
if (optionSchema == null) continue;
//Find matching option input
var optionInput = commandInput.Options.FindByOptionSchema(optionSchema);
//If no option input is available fall back to environment variable values
if (optionInput == null && !string.IsNullOrWhiteSpace(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 (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
continue;
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
}
//No fallback available and no option input was specified, skip option
if (optionInput == null)
continue; continue;
// Convert option to the type of the underlying property
var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType); var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
// Set value of the underlying property // Set value of the underlying property

View File

@@ -12,11 +12,27 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class CommandInputParser : ICommandInputParser public class CommandInputParser : ICommandInputParser
{ {
private readonly IEnvironmentVariablesProvider _environmentVariablesProvider;
/// <summary>
/// Initializes an instance of <see cref="CommandInputParser"/>
/// </summary>
public CommandInputParser(IEnvironmentVariablesProvider environmentVariablesProvider)
{
_environmentVariablesProvider = environmentVariablesProvider;
}
/// <summary>
/// Initializes an instance of <see cref="CommandInputParser"/>
/// </summary>
public CommandInputParser()
: this(new EnvironmentVariablesProvider())
{
}
/// <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>>();
@@ -51,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))
@@ -69,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);
} }
@@ -78,7 +94,9 @@ namespace CliFx.Services
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null; var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray(); var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
return new CommandInput(commandName, directives, options); var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables();
return new CommandInput(commandName, directives, options, environmentVariables);
} }
} }
} }

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>
@@ -31,7 +31,10 @@ namespace CliFx.Services
{ {
} }
private object ConvertValue(string value, Type targetType) /// <summary>
/// Converts a single string value to specified target type.
/// </summary>
protected virtual object? ConvertValue(string value, Type targetType)
{ {
try try
{ {
@@ -41,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))
@@ -108,9 +111,9 @@ namespace CliFx.Services
return Enum.Parse(targetType, value, true); return Enum.Parse(targetType, value, true);
// Nullable // Nullable
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType); 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);
@@ -126,48 +129,63 @@ namespace CliFx.Services
var parseMethod = GetStaticParseMethod(targetType); var parseMethod = GetStaticParseMethod(targetType);
if (parseMethod != null) if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value}); return parseMethod.Invoke(null, new object[] {value});
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
} }
catch (Exception ex) catch (Exception ex)
{ {
// Wrap and rethrow exceptions that occur when trying to convert the value
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].", ex); throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].", ex);
} }
// Throw if we can't find a way to convert the value
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
} }
/// <inheritdoc /> /// <inheritdoc />
public object ConvertOptionInput(CommandOptionInput optionInput, Type targetType) public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
{ {
optionInput.GuardNotNull(nameof(optionInput)); // Get the underlying type of IEnumerable<T> if it's implemented by the target type.
targetType.GuardNotNull(nameof(targetType)); // Ignore string type because it's IEnumerable<T> but we don't treat it as such.
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;
// Single value // Convert to a non-enumerable type
if (optionInput.Values.Count <= 1) if (enumerableUnderlyingType == null)
{ {
// Throw if provided with more than 1 value
if (optionInput.Values.Count > 1)
{
throw new CliFxException(
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
$"to non-enumerable type [{targetType}].");
}
// Retrieve a single value and convert
var value = optionInput.Values.SingleOrDefault(); var value = optionInput.Values.SingleOrDefault();
return ConvertValue(value, targetType); return ConvertValue(value, targetType);
} }
// Multiple values // Convert to an enumerable type
else else
{ {
// Determine underlying type of elements inside the target collection type // Convert values to the underlying enumerable type and cast it to dynamic array
var underlyingType = targetType.GetEnumerableUnderlyingType() ?? typeof(object); var convertedValues = optionInput.Values
.Select(v => ConvertValue(v, enumerableUnderlyingType))
.ToNonGenericArray(enumerableUnderlyingType);
// Convert values to that type // Get the type of produced array
var convertedValues = optionInput.Values.Select(v => ConvertValue(v, underlyingType)).ToNonGenericArray(underlyingType);
var convertedValuesType = convertedValues.GetType(); var convertedValuesType = convertedValues.GetType();
// Assignable from array of values (e.g. T[], IReadOnlyList<T>, IEnumerable<T>) // Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
if (targetType.IsAssignableFrom(convertedValuesType)) if (targetType.IsAssignableFrom(convertedValuesType))
return convertedValues; return convertedValues;
// Has a constructor that accepts an array of values (e.g. HashSet<T>, List<T>) // Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType}); var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType});
if (arrayConstructor != null) if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {convertedValues}); return arrayConstructor.Invoke(new object[] {convertedValues});
// Throw if we can't find a way to convert the values
throw new CliFxException( throw new CliFxException(
$"Can't convert sequence of values [{optionInput.Values.JoinToString(", ")}] to type [{targetType}]."); $"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
$"to type [{targetType}].");
} }
} }
} }

View File

@@ -31,11 +31,12 @@ namespace CliFx.Services
attribute.Name, attribute.Name,
attribute.ShortName, attribute.ShortName,
attribute.IsRequired, attribute.IsRequired,
attribute.Description); attribute.Description,
attribute.EnvironmentVariableName);
// 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)
@@ -67,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

@@ -0,0 +1,27 @@
using System.IO;
using System.Linq;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
/// <inheritdoct />
public class EnvironmentVariablesParser : IEnvironmentVariablesParser
{
/// <inheritdoct />
public CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema)
{
//If the option is not a collection do not split environment variable values
var optionIsCollection = targetOptionSchema.Property != null && targetOptionSchema.Property.PropertyType.IsCollection();
if (!optionIsCollection) return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValue);
//If the option is a collection split the values using System separator, empty values are discarded
var environmentVariableValues = environmentVariableValue.Split(Path.PathSeparator)
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToList();
return new CommandOptionInput(targetOptionSchema.EnvironmentVariableName, environmentVariableValues);
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security;
namespace CliFx.Services
{
/// <inheritdoc />
public class EnvironmentVariablesProvider : IEnvironmentVariablesProvider
{
/// <inheritdoc />
public IReadOnlyDictionary<string, string> GetEnvironmentVariables()
{
try
{
var environmentVariables = Environment.GetEnvironmentVariables();
//Constructing the dictionary manually allows to specify a key comparer that ignores case
//This allows to ignore casing when looking for a fallback environment variable of an option
var environmentVariablesAsDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
//Type DictionaryEntry must be explicitly used otherwise it will enumerate as a collection of objects
foreach (DictionaryEntry environmentVariable in environmentVariables)
{
environmentVariablesAsDictionary.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
}
return environmentVariablesAsDictionary;
}
catch (SecurityException)
{
return new Dictionary<string, string>();
}
}
}
}

View File

@@ -1,5 +1,4 @@
using System; using System;
using CliFx.Internal;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -13,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;
@@ -29,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;
@@ -43,12 +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));
}
} }
} }

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

@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -52,5 +53,11 @@ namespace CliFx.Services
/// Resets foreground and background color to default values. /// Resets foreground and background color to default values.
/// </summary> /// </summary>
void ResetColor(); void ResetColor();
/// <summary>
/// Provides token that cancels when application cancellation is requested.
/// Subsequent calls return the same token.
/// </summary>
CancellationToken GetCancellationToken();
} }
} }

View File

@@ -0,0 +1,15 @@
using CliFx.Models;
namespace CliFx.Services
{
/// <summary>
/// Parses environment variable values
/// </summary>
public interface IEnvironmentVariablesParser
{
/// <summary>
/// Parse an environment variable value and converts it to a <see cref="CommandOptionInput"/>
/// </summary>
CommandOptionInput GetCommandOptionInputFromEnvironmentVariable(string environmentVariableValue, CommandOptionSchema targetOptionSchema);
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace CliFx.Services
{
/// <summary>
/// Provides environment variable values
/// </summary>
public interface IEnvironmentVariablesProvider
{
/// <summary>
/// Returns all the environment variables available.
/// </summary>
/// <remarks>If the User is not allowed to read environment variables it will return an empty dictionary.</remarks>
IReadOnlyDictionary<string, string> GetEnvironmentVariables();
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -8,6 +9,8 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class SystemConsole : IConsole public class SystemConsole : IConsole
{ {
private CancellationTokenSource? _cancellationTokenSource;
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input => Console.In; public TextReader Input => Console.In;
@@ -42,5 +45,26 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public void ResetColor() => Console.ResetColor(); public void ResetColor() => Console.ResetColor();
/// <inheritdoc />
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,6 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using CliFx.Internal; using System.Threading;
namespace CliFx.Services namespace CliFx.Services
{ {
@@ -11,6 +11,8 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class VirtualConsole : IConsole public class VirtualConsole : IConsole
{ {
private readonly CancellationToken _cancellationToken;
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input { get; } public TextReader Input { get; }
@@ -40,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)
{ {
} }
@@ -62,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)
{ {
} }
@@ -71,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)
{ {
} }
@@ -82,5 +87,8 @@ namespace CliFx.Services
ForegroundColor = ConsoleColor.Gray; ForegroundColor = ConsoleColor.Gray;
BackgroundColor = ConsoleColor.Black; BackgroundColor = ConsoleColor.Black;
} }
/// <inheritdoc />
public CancellationToken GetCancellationToken() => _cancellationToken;
} }
} }

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,12 +22,17 @@ _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
- Targets .NET Framework 4.5+ and .NET Standard 2.0+ - Targets .NET Framework 4.5+ and .NET Standard 2.0+
- No external dependencies - No external dependencies
## Screenshots
![help screen](.screenshots/help.png)
## Argument syntax ## Argument syntax
This library employs a variation of the GNU command line argument syntax. Because CliFx uses a context-unaware parser, the syntax rules are generally more consistent and intuitive. This library employs a variation of the GNU command line argument syntax. Because CliFx uses a context-unaware parser, the syntax rules are generally more consistent and intuitive.
@@ -38,8 +41,8 @@ The following examples are valid for any application created with CliFx:
- `myapp --foo bar` sets option `"foo"` to value `"bar"` - `myapp --foo bar` sets option `"foo"` to value `"bar"`
- `myapp -f bar` sets option `'f'` to value `"bar"` - `myapp -f bar` sets option `'f'` to value `"bar"`
- `myapp --switch` sets option `"switch"` to value `true` - `myapp --switch` sets option `"switch"` to value `true`
- `myapp -s` sets option `'s'` to value `true` - `myapp -s` sets option `'s'` to value `true`
- `myapp -abc` sets options `'a'`, `'b'` and `'c'` to value `true` - `myapp -abc` sets options `'a'`, `'b'` and `'c'` to value `true`
- `myapp -xqf bar` sets options `'x'` and `'q'` to value `true`, and option `'f'` to value `"bar"` - `myapp -xqf bar` sets options `'x'` and `'q'` to value `true`, and option `'f'` to value `"bar"`
- `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"` - `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
@@ -95,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:
@@ -123,6 +126,34 @@ When resolving options, CliFx can convert string values obtained from the comman
If you want to define an option of your own type, the easiest way to do it is to make sure that your type is string-initializable, as explained above. If you want to define an option of your own type, the easiest way to do it is to make sure that your type is string-initializable, as explained above.
It is also possible to configure the application to use your own converter, by calling `UseCommandOptionInputConverter` method on `CliApplicationBuilder`.
```c#
var app = new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseCommandOptionInputConverter(new MyConverter())
.Build();
```
The converter class must implement `ICommandOptionInputConverter` but you can also derive from `CommandOptionInputConverter` to extend the default behavior.
```c#
public class MyConverter : CommandOptionInputConverter
{
protected override object ConvertValue(string value, Type targetType)
{
// Custom conversion for MyType
if (targetType == typeof(MyType))
{
// ...
}
// Default behavior for other types
return base.ConvertValue(value, targetType);
}
}
```
### Reporting errors ### Reporting errors
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands. You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
@@ -183,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.
@@ -388,13 +443,12 @@ var app = new CliApplicationBuilder()
## Benchmarks ## Benchmarks
CliFx has the smallest performance overhead compared to other command line parsers and frameworks. Here's how CliFx's execution overhead compares to that of other libraries.
Below you can see a table comparing execution times of a simple command across different libraries.
```ini ```ini
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.14393.0 (1607/AnniversaryUpdate/Redstone1) BenchmarkDotNet=v0.11.5, OS=Windows 10.0.14393.3144 (1607/AnniversaryUpdate/Redstone1)
Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
Frequency=3125008 Hz, Resolution=319.9992 ns, Timer=TSC Frequency=3125011 Hz, Resolution=319.9989 ns, Timer=TSC
.NET Core SDK=2.2.401 .NET Core SDK=2.2.401
[Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT [Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
Core : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT Core : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
@@ -404,10 +458,12 @@ Job=Core Runtime=Core
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | | Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|------------------------------------- |----------:|----------:|----------:|------:|--------:|-----:| |------------------------------------- |----------:|----------:|----------:|------:|--------:|-----:|
| CliFx | 39.47 us | 0.7490 us | 0.9198 us | 1.00 | 0.00 | 1 | | CliFx | 31.29 us | 0.6147 us | 0.7774 us | 1.00 | 0.00 | 2 |
| System.CommandLine | 153.98 us | 0.7112 us | 0.6652 us | 3.90 | 0.09 | 2 | | System.CommandLine | 184.44 us | 3.4993 us | 4.0297 us | 5.90 | 0.21 | 4 |
| McMaster.Extensions.CommandLineUtils | 180.36 us | 3.5893 us | 6.7416 us | 4.59 | 0.16 | 3 | | McMaster.Extensions.CommandLineUtils | 165.50 us | 1.4805 us | 1.3124 us | 5.33 | 0.13 | 3 |
| PowerArgs | 427.54 us | 6.9006 us | 6.4548 us | 10.82 | 0.26 | 4 | | CommandLineParser | 26.65 us | 0.5530 us | 0.5679 us | 0.85 | 0.03 | 1 |
| PowerArgs | 405.44 us | 7.7133 us | 9.1821 us | 12.96 | 0.47 | 6 |
| Clipr | 220.82 us | 4.4567 us | 4.9536 us | 7.06 | 0.25 | 5 |
## Philosophy ## Philosophy
@@ -443,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 2017
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