11 Commits
0.0.4 ... 0.0.6

Author SHA1 Message Date
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
59 changed files with 839 additions and 241 deletions

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,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion>
</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

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -16,6 +17,6 @@ namespace CliFx.Benchmarks.Commands
[CommandOption("bool", 'b')] [CommandOption("bool", 'b')]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

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

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

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
@@ -31,7 +32,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{ {
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set // To make the demo simpler, we will just generate random publish date and ISBN if they were not set
if (Published == default) if (Published == default)

View File

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

View File

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

View File

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

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
{ {
@@ -20,8 +21,8 @@ namespace CliFx.Tests
builder builder
.AddCommand(typeof(HelloWorldDefaultCommand)) .AddCommand(typeof(HelloWorldDefaultCommand))
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly) .AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)}) .AddCommands(new[] { typeof(HelloWorldDefaultCommand) })
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly}) .AddCommandsFrom(new[] { typeof(HelloWorldDefaultCommand).Assembly })
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.AllowDebugMode() .AllowDebugMode()
.AllowPreviewMode() .AllowPreviewMode()
@@ -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,12 @@
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.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
{ {
@@ -17,104 +18,104 @@ namespace CliFx.Tests
private static IEnumerable<TestCaseData> GetTestCases_RunAsync() private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{ {
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] { typeof(HelloWorldDefaultCommand) },
new string[0], new string[0],
"Hello world." "Hello world."
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "}, new[] { "concat", "-i", "foo", "-i", "bar", "-s", " " },
"foo bar" "foo bar"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"concat", "-i", "one", "two", "three", "-s", ", "}, new[] { "concat", "-i", "one", "two", "three", "-s", ", " },
"one, two, three" "one, two, three"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(DivideCommand)}, new[] { typeof(DivideCommand) },
new[] {"div", "-D", "24", "-d", "8"}, new[] { "div", "-D", "24", "-d", "8" },
"3" "3"
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] { typeof(HelloWorldDefaultCommand) },
new[] {"--version"}, new[] { "--version" },
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"--version"}, new[] { "--version" },
TestVersionText TestVersionText
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] { typeof(HelloWorldDefaultCommand) },
new[] {"-h"}, new[] { "-h" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)}, new[] { typeof(HelloWorldDefaultCommand) },
new[] {"--help"}, new[] { "--help" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new string[0], new string[0],
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"-h"}, new[] { "-h" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"--help"}, new[] { "--help" },
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "-h"},
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] { typeof(ConcatCommand) },
new[] {"exc", "-h"}, new[] { "concat", "-h" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] { typeof(ExceptionCommand) },
new[] {"exc", "-h"}, new[] { "exc", "-h" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(CommandExceptionCommand) },
new[] {"[preview]"}, new[] { "exc", "-h" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] { typeof(ConcatCommand) },
new[] {"exc", "[preview]"}, new[] { "[preview]" },
null null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ExceptionCommand) },
new[] {"concat", "[preview]", "-o", "value"}, new[] { "exc", "[preview]" },
null
);
yield return new TestCaseData(
new[] { typeof(ConcatCommand) },
new[] { "concat", "[preview]", "-o", "value" },
null null
); );
} }
@@ -128,38 +129,38 @@ namespace CliFx.Tests
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ConcatCommand)}, new[] { typeof(ConcatCommand) },
new[] {"non-existing"}, new[] { "non-existing" },
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(ExceptionCommand)}, new[] { typeof(ExceptionCommand) },
new[] {"exc"}, new[] { "exc" },
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] { typeof(CommandExceptionCommand) },
new[] {"exc"}, new[] { "exc" },
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] { typeof(CommandExceptionCommand) },
new[] {"exc"}, new[] { "exc" },
null, null null, null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] { typeof(CommandExceptionCommand) },
new[] {"exc", "-m", "foo bar"}, new[] { "exc", "-m", "foo bar" },
"foo bar", null "foo bar", null
); );
yield return new TestCaseData( yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)}, new[] { typeof(CommandExceptionCommand) },
new[] {"exc", "-m", "foo bar", "-c", "666"}, new[] { "exc", "-m", "foo bar", "-c", "666" },
"foo bar", 666 "foo bar", 666
); );
} }
@@ -173,11 +174,13 @@ namespace CliFx.Tests
using (var stdoutStream = new StringWriter()) using (var stdoutStream = new StringWriter())
{ {
var console = new VirtualConsole(stdoutStream); var console = new VirtualConsole(stdoutStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseVersionText(TestVersionText) .UseVersionText(TestVersionText)
.UseConsole(console) .UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build(); .Build();
// Act // Act
@@ -203,10 +206,12 @@ namespace CliFx.Tests
using (var stderrStream = new StringWriter()) using (var stderrStream = new StringWriter())
{ {
var console = new VirtualConsole(TextWriter.Null, stderrStream); var console = new VirtualConsole(TextWriter.Null, stderrStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommands(commandTypes) .AddCommands(commandTypes)
.UseVersionText(TestVersionText) .UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console) .UseConsole(console)
.Build(); .Build();
@@ -219,12 +224,38 @@ namespace CliFx.Tests
exitCode.Should().Be(expectedExitCode); exitCode.Should().Be(expectedExitCode);
else else
exitCode.Should().NotBe(0); exitCode.Should().NotBe(0);
if (expectedStdErr != null) if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr); stderr.Should().Be(expectedStdErr);
else else
stderr.Should().NotBeNullOrWhiteSpace(); stderr.Should().NotBeNullOrWhiteSpace();
} }
} }
[Test]
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using (var stdoutStream = new StringWriter())
{
var console = new VirtualConsole(stdoutStream);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var args = new[] { "cancel" };
// Act
var runTask = application.RunAsync(args);
console.Cancel();
var exitCode = await runTask.ConfigureAwait(false);
var stdOut = stdoutStream.ToString().Trim();
// Assert
exitCode.Should().Be(-2146233029);
stdOut.Should().Be("Printed");
}
}
} }
} }

View File

@@ -7,7 +7,6 @@
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat> <CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput> <CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,12 +1,13 @@
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;
namespace CliFx.Tests.Services namespace CliFx.Tests.Services
{ {
@@ -14,7 +15,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 +27,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 +38,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 +49,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 +59,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 +70,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;B;C;" }
); );
} }

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

@@ -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,23 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async Task ExecuteAsync(IConsole console, CancellationToken cancellationToken)
{
await Task.Yield();
console.Output.WriteLine("Printed");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
console.Output.WriteLine("Never printed");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services; using CliFx.Services;
@@ -14,6 +15,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => throw new CommandException(Message, ExitCode);
} }
} }

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

@@ -0,0 +1,16 @@
using System.Threading;
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, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
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, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
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, CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -11,6 +12,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => throw new Exception(Message);
} }
} }

View File

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

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -13,6 +14,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services; using CliFx.Services;
@@ -10,6 +11,6 @@ namespace CliFx.Tests.TestCommands
[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, CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

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

View File

@@ -28,6 +28,11 @@ namespace CliFx.Attributes
/// </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>
@@ -41,7 +46,7 @@ namespace CliFx.Attributes
/// Initializes an instance of <see cref="CommandOptionAttribute"/>. /// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary> /// </summary>
public CommandOptionAttribute(string name, char shortName) public CommandOptionAttribute(string name, char shortName)
: this(name, (char?) shortName) : this(name, (char?)shortName)
{ {
} }

View File

@@ -171,7 +171,7 @@ namespace CliFx
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput); _commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// Execute command // Execute command
await command.ExecuteAsync(_console); await command.ExecuteAsync(_console, _console.CancellationToken);
// Finish the chain with exit code 0 // Finish the chain with exit code 0
return 0; return 0;

View File

@@ -25,6 +25,8 @@ namespace CliFx
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)
@@ -108,6 +110,20 @@ namespace CliFx
return this; return this;
} }
/// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
{
_commandOptionInputConverter = converter.GuardNotNull(nameof(converter));
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider)
{
_environmentVariablesProvider = environmentVariablesProvider.GuardNotNull(nameof(environmentVariablesProvider));
return this;
}
/// <inheritdoc /> /// <inheritdoc />
public ICliApplication Build() public ICliApplication Build()
{ {
@@ -117,14 +133,16 @@ namespace CliFx
_versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0"; _versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0";
_console = _console ?? new SystemConsole(); _console = _console ?? new SystemConsole();
_commandFactory = _commandFactory ?? new CommandFactory(); _commandFactory = _commandFactory ?? new CommandFactory();
_commandOptionInputConverter = _commandOptionInputConverter ?? new CommandOptionInputConverter();
_environmentVariablesProvider = _environmentVariablesProvider ?? new EnvironmentVariablesProvider();
// Project parameters to expected types // Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description); var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
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());
} }
} }

View File

@@ -2,8 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks> <TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion> <Version>0.0.6</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,7 +10,7 @@
<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> <RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -20,4 +19,8 @@
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
</Project> <ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup>
</Project>

View File

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

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

View File

@@ -36,8 +36,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 GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
public static Type GetEnumerableUnderlyingType(this Type 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);

View File

@@ -25,14 +25,36 @@ 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; // can be null
Directives = directives.GuardNotNull(nameof(directives)); Directives = directives.GuardNotNull(nameof(directives));
Options = options.GuardNotNull(nameof(options)); Options = options.GuardNotNull(nameof(options));
EnvironmentVariables = environmentVariables.GuardNotNull(nameof(environmentVariables));
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
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> /// <summary>
@@ -87,6 +109,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

@@ -34,16 +34,22 @@ namespace CliFx.Models
/// </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; // can be null
Name = name; // can be null Name = name; // can be null
ShortName = shortName; // can be null ShortName = shortName; // can be null
IsRequired = isRequired; IsRequired = isRequired;
Description = description; // can be null Description = description; // can be null
EnvironmentVariableName = environmentVariableName; //can be null
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -75,9 +81,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

@@ -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
{ {
@@ -71,16 +71,16 @@ 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.GuardNotNull(nameof(optionInputs));
optionSchema.GuardNotNull(nameof(optionSchema));
return optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias));
} }
/// <summary> /// <summary>

View File

@@ -11,20 +11,30 @@ 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.GuardNotNull(nameof(commandOptionInputConverter)); _commandOptionInputConverter = commandOptionInputConverter.GuardNotNull(nameof(commandOptionInputConverter));
_environmentVariablesParser = environmentVariablesParser.GuardNotNull(nameof(environmentVariablesParser));
}
/// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser)
: this(new CommandOptionInputConverter(), environmentVariablesParser)
{
} }
/// <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())
{ {
} }
@@ -38,15 +48,28 @@ namespace CliFx.Services
// 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 //Find matching option input
var optionSchema = commandSchema.Options.FindByAlias(optionInput.Alias); var optionInput = commandInput.Options.FindByOptionSchema(optionSchema);
if (optionSchema == null)
//If no option input is available fall back to environment variable values
if (optionInput == null && !optionSchema.EnvironmentVariableName.IsNullOrWhiteSpace())
{
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 || commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName].IsNullOrWhiteSpace())
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,6 +12,26 @@ 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.GuardNotNull(nameof(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)
{ {
@@ -78,7 +98,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

@@ -31,8 +31,13 @@ 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)
{ {
targetType.GuardNotNull(nameof(targetType));
try try
{ {
// String or object // String or object
@@ -108,7 +113,7 @@ 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 !value.IsNullOrWhiteSpace() ? ConvertValue(value, nullableUnderlyingType) : null;
@@ -126,48 +131,66 @@ 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)); optionInput.GuardNotNull(nameof(optionInput));
targetType.GuardNotNull(nameof(targetType)); targetType.GuardNotNull(nameof(targetType));
// Single value // Get the underlying type of IEnumerable<T> if it's implemented by the target type.
if (optionInput.Values.Count <= 1) // Ignore string type because it's IEnumerable<T> but we don't treat it as such.
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;
// Convert to a non-enumerable type
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,7 +31,8 @@ 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

View File

@@ -0,0 +1,30 @@
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)
{
environmentVariableValue.GuardNotNull(nameof(environmentVariableValue));
targetOptionSchema.GuardNotNull(nameof(targetOptionSchema));
//If the option is not a collection do not split environment variable values
var optionIsCollection = 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 => !v.IsNullOrWhiteSpace())
.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,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using CliFx.Internal; using CliFx.Internal;
namespace CliFx.Services namespace CliFx.Services
@@ -50,5 +51,25 @@ namespace CliFx.Services
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action)); console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
} }
/// <summary>
/// Gets wether a string representing an environment variable value is escaped (i.e.: surrounded by double quotation marks)
/// </summary>
public static bool IsEnvironmentVariableEscaped(this string environmentVariableValue)
{
environmentVariableValue.GuardNotNull(nameof(environmentVariableValue));
return environmentVariableValue.StartsWith("\"") && environmentVariableValue.EndsWith("\"");
}
/// <summary>
/// Gets wether the <see cref="Type"/> supplied is a collection implementing <see cref="IEnumerable{T}"/>
/// </summary>
public static bool IsCollection(this Type type)
{
type.GuardNotNull(nameof(type));
return type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
}
} }
} }

View File

@@ -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,10 @@ 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>
/// Cancels when soft cancellation requested.
/// </summary>
CancellationToken CancellationToken { get; }
} }
} }

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,22 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class SystemConsole : IConsole public class SystemConsole : IConsole
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
/// <inheritdoc />
public SystemConsole()
{
// Subscribe to CancelKeyPress event with cancellation token source
// Kills app on second cancellation (hard cancellation)
Console.CancelKeyPress += (_, args) =>
{
if (_cancellationTokenSource.IsCancellationRequested)
return;
args.Cancel = true;
_cancellationTokenSource.Cancel();
};
}
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input => Console.In; public TextReader Input => Console.In;
@@ -42,5 +59,8 @@ namespace CliFx.Services
/// <inheritdoc /> /// <inheritdoc />
public void ResetColor() => Console.ResetColor(); public void ResetColor() => Console.ResetColor();
/// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
} }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using CliFx.Internal; using CliFx.Internal;
namespace CliFx.Services namespace CliFx.Services
@@ -11,6 +12,8 @@ namespace CliFx.Services
/// </summary> /// </summary>
public class VirtualConsole : IConsole public class VirtualConsole : IConsole
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
/// <inheritdoc /> /// <inheritdoc />
public TextReader Input { get; } public TextReader Input { get; }
@@ -82,5 +85,16 @@ namespace CliFx.Services
ForegroundColor = ConsoleColor.Gray; ForegroundColor = ConsoleColor.Gray;
BackgroundColor = ConsoleColor.Black; BackgroundColor = ConsoleColor.Black;
} }
/// <inheritdoc />
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
/// <summary>
/// Simulates cancellation.
/// </summary>
public void Cancel()
{
_cancellationTokenSource.Cancel();
}
} }
} }

View File

@@ -30,6 +30,10 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
- 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 +42,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"`
@@ -123,6 +127,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.
@@ -388,13 +420,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 +435,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

View File

@@ -1,6 +1,6 @@
version: '{build}' version: '{build}'
image: Visual Studio 2017 image: Visual Studio 2019
configuration: Release configuration: Release
before_build: before_build: