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_*
.*crunch*.local.xml
nCrunchTemp_*
.ncrunchsolution
# MightyMoose
*.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]
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)]
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
@@ -19,16 +19,17 @@ namespace CliFx.Benchmarks
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
// Skipped because this benchmark freezes after a couple of iterations
// Probably wasn't designed to run multiple times in single process execution
//[Benchmark(Description = "CommandLineParser")]
[Benchmark(Description = "CommandLineParser")]
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());
}
[Benchmark(Description = "PowerArgs")]
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>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<LangVersion>latest</LangVersion>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
<PackageReference Include="PowerArgs" Version="3.6.0" />

View File

@@ -8,7 +8,7 @@ namespace CliFx.Benchmarks.Commands
public class CliFxCommand : ICommand
{
[CommandOption("str", 's')]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[CommandOption("int", 'i')]
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
{
[Option('s', "str")]
public string StrOption { get; set; }
public string? StrOption { get; set; }
[Option('i', "int")]
public int IntOption { get; set; }

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ namespace CliFx.Demo.Commands
public DateTimeOffset Published { get; set; }
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; set; }
public Isbn? Isbn { get; set; }
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.Services;
using Microsoft.Extensions.DependencyInjection;
@@ -7,7 +8,7 @@ namespace CliFx.Demo
{
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
var services = new ServiceCollection();
@@ -21,7 +22,12 @@ namespace CliFx.Demo
services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider();
return services.BuildServiceProvider();
}
public static Task<int> Main(string[] args)
{
var serviceProvider = ConfigureServices();
return new CliApplicationBuilder()
.AddCommandsFromThisAssembly()

View File

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

View File

@@ -1,11 +1,13 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Services;
using CliFx.Tests.Stubs;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
@@ -167,64 +169,92 @@ namespace CliFx.Tests
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
string expectedStdOut = null)
string? expectedStdOut = null)
{
// Arrange
using (var stdoutStream = new StringWriter())
{
var console = new VirtualConsole(stdoutStream);
await using var stdoutStream = new StringWriter();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
var console = new VirtualConsole(stdoutStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stdOut = stdoutStream.ToString().Trim();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseConsole(console)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.Build();
// Assert
exitCode.Should().Be(0);
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stdOut = stdoutStream.ToString().Trim();
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
else
stdOut.Should().NotBeNullOrWhiteSpace();
}
// Assert
exitCode.Should().Be(0);
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
else
stdOut.Should().NotBeNullOrWhiteSpace();
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
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
using (var stderrStream = new StringWriter())
{
var console = new VirtualConsole(TextWriter.Null, stderrStream);
await using var stderrStream = new StringWriter();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
var console = new VirtualConsole(TextWriter.Null, stderrStream);
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stderr = stderrStream.ToString().Trim();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseVersionText(TestVersionText)
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
.UseConsole(console)
.Build();
// Assert
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
else
exitCode.Should().NotBe(0);
// Act
var exitCode = await application.RunAsync(commandLineArguments);
var stderr = stderrStream.ToString().Trim();
if (expectedStdErr != null)
stderr.Should().Be(expectedStdErr);
else
stderr.Should().NotBeNullOrWhiteSpace();
}
// Assert
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
else
exitCode.Should().NotBe(0);
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">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="coverlet.msbuild" Version="2.7.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,12 +1,14 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
using CliFx.Tests.Stubs;
using System.IO;
namespace CliFx.Tests.Services
{
@@ -14,7 +16,7 @@ namespace CliFx.Tests.Services
public class CommandInitializerTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single();
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
{
@@ -26,7 +28,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8")
}),
new DivideCommand {Dividend = 13, Divisor = 8}
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
@@ -37,7 +39,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8")
}),
new DivideCommand {Dividend = 13, Divisor = 8}
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
@@ -48,7 +50,7 @@ namespace CliFx.Tests.Services
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8")
}),
new DivideCommand {Dividend = 13, Divisor = 8}
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
@@ -58,7 +60,7 @@ namespace CliFx.Tests.Services
{
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
}),
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}}
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
);
yield return new TestCaseData(
@@ -69,7 +71,43 @@ namespace CliFx.Tests.Services
new CommandOptionInput("i", new[] {"foo", "bar"}),
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.Services;
using FluentAssertions;
using NUnit.Framework;
using CliFx.Tests.Stubs;
namespace CliFx.Tests.Services
{
@@ -11,203 +12,238 @@ namespace CliFx.Tests.Services
{
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(
new[] {"--option", "value"},
new[] { "--option", "value" },
new CommandInput(new[]
{
new CommandOptionInput("option", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--option1", "value1", "--option2", "value2"},
new[] { "--option1", "value1", "--option2", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("option2", "value2")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--option", "value1", "value2"},
new[] { "--option", "value1", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--option", "value1", "--option", "value2"},
new[] { "--option", "value1", "--option", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-a", "value"},
new[] { "-a", "value" },
new CommandInput(new[]
{
new CommandOptionInput("a", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-a", "value1", "-b", "value2"},
new[] { "-a", "value1", "-b", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", "value1"),
new CommandOptionInput("b", "value2")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-a", "value1", "value2"},
new[] { "-a", "value1", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-a", "value1", "-a", "value2"},
new[] { "-a", "value1", "-a", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--option1", "value1", "-b", "value2"},
new[] { "--option1", "value1", "-b", "value2" },
new CommandInput(new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("b", "value2")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--switch"},
new[] { "--switch" },
new CommandInput(new[]
{
new CommandOptionInput("switch")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"--switch1", "--switch2"},
new[] { "--switch1", "--switch2" },
new CommandInput(new[]
{
new CommandOptionInput("switch1"),
new CommandOptionInput("switch2")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-s"},
new[] { "-s" },
new CommandInput(new[]
{
new CommandOptionInput("s")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-a", "-b"},
new[] { "-a", "-b" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-ab"},
new[] { "-ab" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"-ab", "value"},
new[] { "-ab", "value" },
new CommandInput(new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"command"},
new CommandInput("command")
new[] { "command" },
new CommandInput("command"),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"command", "--option", "value"},
new[] { "command", "--option", "value" },
new CommandInput("command", new[]
{
new CommandOptionInput("option", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"long", "command", "name"},
new CommandInput("long command name")
new[] { "long", "command", "name" },
new CommandInput("long command name"),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"long", "command", "name", "--option", "value"},
new[] { "long", "command", "name", "--option", "value" },
new CommandInput("long command name", new[]
{
new CommandOptionInput("option", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"[debug]"},
new[] { "[debug]" },
new CommandInput(null,
new[] {"debug"},
new CommandOptionInput[0])
new[] { "debug" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]"},
new[] { "[debug]", "[preview]" },
new CommandInput(null,
new[] {"debug", "preview"},
new CommandOptionInput[0])
new[] { "debug", "preview" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]", "-o", "value"},
new[] { "[debug]", "[preview]", "-o", "value" },
new CommandInput(null,
new[] {"debug", "preview"},
new[] { "debug", "preview" },
new[]
{
new CommandOptionInput("o", "value")
})
}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] {"command", "[debug]", "[preview]", "-o", "value"},
new[] { "command", "[debug]", "[preview]", "-o", "value" },
new CommandInput("command",
new[] {"debug", "preview"},
new[] { "debug", "preview" },
new[]
{
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]
[TestCaseSource(nameof(GetTestCases_ParseCommandInput))]
public void ParseCommandInput_Test(IReadOnlyList<string> commandLineArguments,
CommandInput expectedCommandInput)
CommandInput expectedCommandInput, IEnvironmentVariablesProvider environmentVariablesProvider)
{
// Arrange
var parser = new CommandInputParser();
var parser = new CommandInputParser(environmentVariablesProvider);
// Act
var commandInput = parser.ParseCommandInput(commandLineArguments);

View File

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

View File

@@ -1,11 +1,11 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
@@ -15,30 +15,37 @@ namespace CliFx.Tests.Services
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
{
yield return new TestCaseData(
new[] {typeof(DivideCommand), typeof(ConcatCommand)},
new[] { typeof(DivideCommand), typeof(ConcatCommand), typeof(EnvironmentVariableCommand) },
new[]
{
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new[]
{
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)),
"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[]
{
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)),
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(
new[] {typeof(HelloWorldDefaultCommand)},
new[] { typeof(HelloWorldDefaultCommand) },
new[]
{
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])

View File

@@ -18,7 +18,7 @@ namespace CliFx.Tests.Services
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{
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))
);
}

View File

@@ -93,17 +93,16 @@ namespace CliFx.Tests.Services
IReadOnlyList<string> expectedSubstrings)
{
// Arrange
using (var stdout = new StringWriter())
{
var console = new VirtualConsole(stdout);
var renderer = new HelpTextRenderer();
using var stdout = new StringWriter();
// Act
renderer.RenderHelpText(console, source);
var console = new VirtualConsole(stdout);
var renderer = new HelpTextRenderer();
// Assert
stdout.ToString().Should().ContainAll(expectedSubstrings);
}
// Act
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()
{
// Arrange
using (var stdin = new StringReader("hello world"))
using (var stdout = new StringWriter())
using (var stderr = new StringWriter())
{
var console = new VirtualConsole(stdin, stdout, stderr);
using var stdin = new StringReader("hello world");
using var stdout = new StringWriter();
using var stderr = new StringWriter();
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
var console = new VirtualConsole(stdin, stdout, stderr);
// Assert
console.Input.Should().BeSameAs(stdin);
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().BeSameAs(stdin);
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().BeSameAs(stdout);
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().BeSameAs(stderr);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
}
}

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;
[CommandOption("msg", 'm')]
public string Message { get; set; }
public string? Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
}

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.TestCommands
public class DuplicateOptionNamesCommand : ICommand
{
[CommandOption("fruits")]
public string Apples { get; set; }
public string? Apples { get; set; }
[CommandOption("fruits")]
public string Oranges { get; set; }
public string? Oranges { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.TestCommands
public class DuplicateOptionShortNamesCommand : ICommand
{
[CommandOption('f')]
public string Apples { get; set; }
public string? Apples { get; set; }
[CommandOption('f')]
public string Oranges { get; set; }
public string? Oranges { 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 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
{
[CommandOption("msg", 'm')]
public string Message { get; set; }
public string? Message { get; set; }
public Task ExecuteAsync(IConsole console) => throw new Exception(Message);
}

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.TestCommands
public class HelpDefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string OptionA { get; set; }
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string OptionB { get; set; }
public string? OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}

View File

@@ -8,10 +8,10 @@ namespace CliFx.Tests.TestCommands
public class HelpNamedCommand : ICommand
{
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string OptionC { get; set; }
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string OptionD { get; set; }
public string? OptionD { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}

View File

@@ -8,7 +8,7 @@ namespace CliFx.Tests.TestCommands
public class HelpSubCommand : ICommand
{
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string OptionE { get; set; }
public string? OptionE { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}

View File

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

View File

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

View File

@@ -10,11 +10,13 @@ namespace CliFx.Attributes
{
/// <summary>
/// Option name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// </summary>
public string Name { get; }
public string? Name { get; }
/// <summary>
/// Option short name.
/// Either <see cref="Name"/> or <see cref="ShortName"/> must be set.
/// </summary>
public char? ShortName { get; }
@@ -26,15 +28,20 @@ namespace CliFx.Attributes
/// <summary>
/// Option description, which is used in help text.
/// </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>
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
public CommandOptionAttribute(string name, char? shortName)
private CommandOptionAttribute(string? name, char? shortName)
{
Name = name; // can be null
ShortName = shortName; // can be null
Name = name;
ShortName = shortName;
}
/// <summary>
@@ -57,7 +64,7 @@ namespace CliFx.Attributes
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
/// </summary>
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,
ICommandFactory commandFactory, ICommandInitializer commandInitializer, IHelpTextRenderer helpTextRenderer)
{
_metadata = metadata.GuardNotNull(nameof(metadata));
_configuration = configuration.GuardNotNull(nameof(configuration));
_metadata = metadata;
_configuration = configuration;
_console = console.GuardNotNull(nameof(console));
_commandInputParser = commandInputParser.GuardNotNull(nameof(commandInputParser));
_commandSchemaResolver = commandSchemaResolver.GuardNotNull(nameof(commandSchemaResolver));
_commandFactory = commandFactory.GuardNotNull(nameof(commandFactory));
_commandInitializer = commandInitializer.GuardNotNull(nameof(commandInitializer));
_helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer));
_console = console;
_commandInputParser = commandInputParser;
_commandSchemaResolver = commandSchemaResolver;
_commandFactory = commandFactory;
_commandInitializer = commandInitializer;
_helpTextRenderer = helpTextRenderer;
}
private async Task<int?> HandleDebugDirectiveAsync(CommandInput commandInput)
@@ -117,7 +117,7 @@ namespace CliFx
}
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
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null;
@@ -180,8 +180,6 @@ namespace CliFx
/// <inheritdoc />
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
try
{
// Parse command input from arguments
@@ -199,7 +197,7 @@ namespace CliFx
HandlePreviewDirective(commandInput) ??
HandleVersionOption(commandInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
await HandleCommandExecutionAsync(commandInput, targetCommandSchema);
await HandleCommandExecutionAsync(commandInput, targetCommandSchema!);
}
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.
// 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));
}

View File

@@ -19,18 +19,18 @@ namespace CliFx
private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true;
private string _title;
private string _executableName;
private string _versionText;
private string _description;
private IConsole _console;
private ICommandFactory _commandFactory;
private string? _title;
private string? _executableName;
private string? _versionText;
private string? _description;
private IConsole? _console;
private ICommandFactory? _commandFactory;
private ICommandOptionInputConverter? _commandOptionInputConverter;
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
/// <inheritdoc />
public ICliApplicationBuilder AddCommand(Type commandType)
{
commandType.GuardNotNull(nameof(commandType));
_commandTypes.Add(commandType);
return this;
@@ -39,8 +39,6 @@ namespace CliFx
/// <inheritdoc />
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{
commandAssembly.GuardNotNull(nameof(commandAssembly));
var commandTypes = commandAssembly.ExportedTypes
.Where(t => t.Implements(typeof(ICommand)))
.Where(t => t.IsDefined(typeof(CommandAttribute)))
@@ -69,42 +67,56 @@ namespace CliFx
/// <inheritdoc />
public ICliApplicationBuilder UseTitle(string title)
{
_title = title.GuardNotNull(nameof(title));
_title = title;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseExecutableName(string executableName)
{
_executableName = executableName.GuardNotNull(nameof(executableName));
_executableName = executableName;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseVersionText(string versionText)
{
_versionText = versionText.GuardNotNull(nameof(versionText));
_versionText = versionText;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseDescription(string description)
public ICliApplicationBuilder UseDescription(string? description)
{
_description = description; // can be null
_description = description;
return this;
}
/// <inheritdoc />
public ICliApplicationBuilder UseConsole(IConsole console)
{
_console = console.GuardNotNull(nameof(console));
_console = console;
return this;
}
/// <inheritdoc />
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;
}
@@ -112,19 +124,21 @@ namespace CliFx
public ICliApplication Build()
{
// Use defaults for required parameters that were not configured
_title = _title ?? GetDefaultTitle() ?? "App";
_executableName = _executableName ?? GetDefaultExecutableName() ?? "app";
_versionText = _versionText ?? GetDefaultVersionText() ?? "v1.0";
_console = _console ?? new SystemConsole();
_commandFactory = _commandFactory ?? new CommandFactory();
_title ??= GetDefaultTitle() ?? "App";
_executableName ??= GetDefaultExecutableName() ?? "app";
_versionText ??= GetDefaultVersionText() ?? "v1.0";
_console ??= new SystemConsole();
_commandFactory ??= new CommandFactory();
_commandOptionInputConverter ??= new CommandOptionInputConverter();
_environmentVariablesProvider ??= new EnvironmentVariablesProvider();
// Project parameters to expected types
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration,
_console, new CommandInputParser(), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(), new HelpTextRenderer());
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
}
}
@@ -135,7 +149,7 @@ namespace CliFx
// Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value;
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name;
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name ?? "";
private static string GetDefaultExecutableName()
{
@@ -151,6 +165,6 @@ namespace CliFx
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">
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.0.4</Version>
<TargetFrameworks>net45;netstandard2.0;netstandard2.1</TargetFrameworks>
<Version>0.0.8</Version>
<Company>Tyrrrz</Company>
<Authors>$(Company)</Authors>
<Copyright>Copyright (C) Alexey Golub</Copyright>
@@ -11,13 +10,27 @@
<PackageTags>command line executable interface framework parser arguments net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<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>
<RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PublishRepositoryUrl>True</PublishRepositoryUrl>
<EmbedUntrackedSources>True</EmbedUntrackedSources>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.1.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="../favicon.png" Pack="True" PackagePath="" />
</ItemGroup>
</Project>

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using CliFx.Internal;
namespace CliFx.Exceptions
{
@@ -20,16 +19,16 @@ namespace CliFx.Exceptions
/// <summary>
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public CommandException(string message, Exception innerException, int exitCode = DefaultExitCode)
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode)
: 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>
/// Initializes an instance of <see cref="CommandException"/>.
/// </summary>
public CommandException(string message, int exitCode = DefaultExitCode)
public CommandException(string? message, int exitCode = DefaultExitCode)
: this(message, null, exitCode)
{
}

View File

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

View File

@@ -47,7 +47,7 @@ namespace CliFx
/// <summary>
/// Sets application description, which appears in the help text.
/// </summary>
ICliApplicationBuilder UseDescription(string description);
ICliApplicationBuilder UseDescription(string? description);
/// <summary>
/// Configures application to use specified implementation of <see cref="IConsole"/>.
@@ -59,6 +59,16 @@ namespace CliFx
/// </summary>
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>
/// Creates an instance of <see cref="ICliApplication"/> using configured parameters.
/// 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
{
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 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 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))
return typeof(object);
@@ -60,5 +63,8 @@ namespace CliFx.Internal
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.Collections.Generic;
using CliFx.Internal;
namespace CliFx.Models
{
@@ -30,7 +29,7 @@ namespace CliFx.Models
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes,
bool isDebugModeAllowed, bool isPreviewModeAllowed)
{
CommandTypes = commandTypes.GuardNotNull(nameof(commandTypes));
CommandTypes = commandTypes;
IsDebugModeAllowed = isDebugModeAllowed;
IsPreviewModeAllowed = isPreviewModeAllowed;
}

View File

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

View File

@@ -13,7 +13,7 @@ namespace CliFx.Models
/// Specified command name.
/// Can be null if command was not specified.
/// </summary>
public string CommandName { get; }
public string? CommandName { get; }
/// <summary>
/// Specified directives.
@@ -25,20 +25,43 @@ namespace CliFx.Models
/// </summary>
public IReadOnlyList<CommandOptionInput> Options { get; }
/// <summary>
/// Environment variables available when the command was parsed
/// </summary>
public IReadOnlyDictionary<string, string> EnvironmentVariables { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </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
Directives = directives.GuardNotNull(nameof(directives));
Options = options.GuardNotNull(nameof(options));
CommandName = commandName;
Directives = directives;
Options = options;
EnvironmentVariables = environmentVariables;
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </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)
{
}
@@ -54,7 +77,7 @@ namespace CliFx.Models
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string commandName)
public CommandInput(string? commandName)
: this(commandName, EmptyOptions)
{
}
@@ -64,7 +87,7 @@ namespace CliFx.Models
{
var buffer = new StringBuilder();
if (!CommandName.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(CommandName))
buffer.Append(CommandName);
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<CommandOptionInput> EmptyOptions = new CommandOptionInput[0];
private static readonly IReadOnlyDictionary<string, string> EmptyEnvironmentVariables = new Dictionary<string, string>();
/// <summary>
/// Empty input.

View File

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

View File

@@ -1,6 +1,5 @@
using System.Reflection;
using System.Text;
using CliFx.Internal;
namespace CliFx.Models
{
@@ -12,12 +11,12 @@ namespace CliFx.Models
/// <summary>
/// Underlying property.
/// </summary>
public PropertyInfo Property { get; }
public PropertyInfo? Property { get; }
/// <summary>
/// Option name.
/// </summary>
public string Name { get; }
public string? Name { get; }
/// <summary>
/// Option short name.
@@ -32,18 +31,24 @@ namespace CliFx.Models
/// <summary>
/// Option description.
/// </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>
/// Initializes an instance of <see cref="CommandOptionSchema"/>.
/// </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
Name = name; // can be null
ShortName = shortName; // can be null
Property = property;
Name = name;
ShortName = shortName;
IsRequired = isRequired;
Description = description; // can be null
Description = description;
EnvironmentVariableName = environmentVariableName;
}
/// <inheritdoc />
@@ -54,10 +59,10 @@ namespace CliFx.Models
if (IsRequired)
buffer.Append('*');
if (!Name.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
if (!Name.IsNullOrWhiteSpace() && ShortName != null)
if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
buffer.Append('|');
if (ShortName != null)
@@ -75,9 +80,9 @@ namespace CliFx.Models
// ...in CliApplication (when reading) and HelpTextRenderer (when writing).
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; } =
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>
/// Underlying type.
/// </summary>
public Type Type { get; }
public Type? Type { get; }
/// <summary>
/// Command name.
/// </summary>
public string Name { get; }
public string? Name { get; }
/// <summary>
/// Command description.
/// </summary>
public string Description { get; }
public string? Description { get; }
/// <summary>
/// Command options.
@@ -33,12 +33,12 @@ namespace CliFx.Models
/// <summary>
/// Initializes an instance of <see cref="CommandSchema"/>.
/// </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
Name = name; // can be null
Description = description; // can be null
Options = options.GuardNotNull(nameof(options));
Type = type;
Name = name;
Description = description;
Options = options;
}
/// <inheritdoc />
@@ -46,7 +46,7 @@ namespace CliFx.Models
{
var buffer = new StringBuilder();
if (!Name.IsNullOrWhiteSpace())
if (!string.IsNullOrWhiteSpace(Name))
buffer.Append(Name);
foreach (var option in Options)

View File

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

View File

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

View File

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

View File

@@ -11,42 +11,65 @@ namespace CliFx.Services
public class CommandInitializer : ICommandInitializer
{
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
private readonly IEnvironmentVariablesParser _environmentVariablesParser;
/// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </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>
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer()
: this(new CommandOptionInputConverter())
: this(new CommandOptionInputConverter(), new EnvironmentVariablesParser())
{
}
/// <inheritdoc />
public void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
{
command.GuardNotNull(nameof(command));
commandSchema.GuardNotNull(nameof(commandSchema));
commandInput.GuardNotNull(nameof(commandInput));
// Keep track of unset required options to report an error at a later stage
var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList();
// Set command options
foreach (var optionInput in commandInput.Options)
//Set command options
foreach (var optionSchema in commandSchema.Options)
{
// Find matching option schema for this option input
var optionSchema = commandSchema.Options.FindByAlias(optionInput.Alias);
if (optionSchema == null)
// Ignore special options that are not backed by a property
if (optionSchema.Property == 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;
// Convert option to the type of the underlying property
var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
// Set value of the underlying property

View File

@@ -12,11 +12,27 @@ namespace CliFx.Services
/// </summary>
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 />
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
{
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
var commandNameBuilder = new StringBuilder();
var directives = new List<string>();
var optionsDic = new Dictionary<string, List<string>>();
@@ -51,7 +67,7 @@ namespace CliFx.Services
}
// Encountered directive or (part of) command name
else if (lastOptionAlias.IsNullOrWhiteSpace())
else if (string.IsNullOrWhiteSpace(lastOptionAlias))
{
if (commandLineArgument.StartsWith("[", StringComparison.OrdinalIgnoreCase) &&
commandLineArgument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
@@ -69,7 +85,7 @@ namespace CliFx.Services
}
// Encountered option value
else if (!lastOptionAlias.IsNullOrWhiteSpace())
else if (!string.IsNullOrWhiteSpace(lastOptionAlias))
{
optionsDic[lastOptionAlias].Add(commandLineArgument);
}
@@ -78,7 +94,9 @@ namespace CliFx.Services
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
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>
public CommandOptionInputConverter(IFormatProvider formatProvider)
{
_formatProvider = formatProvider.GuardNotNull(nameof(formatProvider));
_formatProvider = formatProvider;
}
/// <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
{
@@ -41,7 +44,7 @@ namespace CliFx.Services
// Bool
if (targetType == typeof(bool))
return value.IsNullOrWhiteSpace() || bool.Parse(value);
return string.IsNullOrWhiteSpace(value) || bool.Parse(value);
// Char
if (targetType == typeof(char))
@@ -108,9 +111,9 @@ namespace CliFx.Services
return Enum.Parse(targetType, value, true);
// Nullable
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
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
var stringConstructor = GetStringConstructor(targetType);
@@ -126,48 +129,63 @@ namespace CliFx.Services
var parseMethod = GetStaticParseMethod(targetType);
if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value});
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
}
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 if we can't find a way to convert the value
throw new CliFxException($"Can't convert value [{value}] to type [{targetType}].");
}
/// <inheritdoc />
public object ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
{
optionInput.GuardNotNull(nameof(optionInput));
targetType.GuardNotNull(nameof(targetType));
// Get the underlying type of IEnumerable<T> if it's implemented by the target type.
// 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
if (optionInput.Values.Count <= 1)
// 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();
return ConvertValue(value, targetType);
}
// Multiple values
// Convert to an enumerable type
else
{
// Determine underlying type of elements inside the target collection type
var underlyingType = targetType.GetEnumerableUnderlyingType() ?? typeof(object);
// Convert values to the underlying enumerable type and cast it to dynamic array
var convertedValues = optionInput.Values
.Select(v => ConvertValue(v, enumerableUnderlyingType))
.ToNonGenericArray(enumerableUnderlyingType);
// Convert values to that type
var convertedValues = optionInput.Values.Select(v => ConvertValue(v, underlyingType)).ToNonGenericArray(underlyingType);
// Get the type of produced array
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))
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});
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {convertedValues});
// Throw if we can't find a way to convert the values
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.ShortName,
attribute.IsRequired,
attribute.Description);
attribute.Description,
attribute.EnvironmentVariableName);
// Make sure there are no other options with the same name
var existingOptionWithSameName = result
.Where(o => !o.Name.IsNullOrWhiteSpace())
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
.FirstOrDefault(o => string.Equals(o.Name, optionSchema.Name, StringComparison.OrdinalIgnoreCase));
if (existingOptionWithSameName != null)
@@ -67,8 +68,6 @@ namespace CliFx.Services
/// <inheritdoc />
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
{
commandTypes.GuardNotNull(nameof(commandTypes));
// Make sure there's at least one command defined
if (!commandTypes.Any())
{

View File

@@ -1,5 +1,4 @@
using System;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
@@ -16,14 +15,10 @@ namespace CliFx.Services
/// </summary>
public DelegateCommandFactory(Func<CommandSchema, ICommand> factoryMethod)
{
_factoryMethod = factoryMethod.GuardNotNull(nameof(factoryMethod));
_factoryMethod = factoryMethod;
}
/// <inheritdoc />
public ICommand CreateCommand(CommandSchema commandSchema)
{
commandSchema.GuardNotNull(nameof(commandSchema));
return _factoryMethod(commandSchema);
}
public ICommand CreateCommand(CommandSchema commandSchema) => _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 CliFx.Internal;
namespace CliFx.Services
{
@@ -13,9 +12,6 @@ namespace CliFx.Services
/// </summary>
public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
{
console.GuardNotNull(nameof(console));
action.GuardNotNull(nameof(action));
var lastColor = console.ForegroundColor;
console.ForegroundColor = foregroundColor;
@@ -29,9 +25,6 @@ namespace CliFx.Services
/// </summary>
public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
{
console.GuardNotNull(nameof(console));
action.GuardNotNull(nameof(action));
var lastColor = console.BackgroundColor;
console.BackgroundColor = backgroundColor;
@@ -43,12 +36,7 @@ namespace CliFx.Services
/// <summary>
/// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
/// </summary>
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action)
{
console.GuardNotNull(nameof(console));
action.GuardNotNull(nameof(action));
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action) =>
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
namespace CliFx.Services
{
@@ -52,5 +53,11 @@ namespace CliFx.Services
/// Resets foreground and background color to default values.
/// </summary>
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.IO;
using System.Threading;
namespace CliFx.Services
{
@@ -8,6 +9,8 @@ namespace CliFx.Services
/// </summary>
public class SystemConsole : IConsole
{
private CancellationTokenSource? _cancellationTokenSource;
/// <inheritdoc />
public TextReader Input => Console.In;
@@ -42,5 +45,26 @@ namespace CliFx.Services
/// <inheritdoc />
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.IO;
using CliFx.Internal;
using System.Threading;
namespace CliFx.Services
{
@@ -11,6 +11,8 @@ namespace CliFx.Services
/// </summary>
public class VirtualConsole : IConsole
{
private readonly CancellationToken _cancellationToken;
/// <inheritdoc />
public TextReader Input { get; }
@@ -40,21 +42,24 @@ namespace CliFx.Services
/// </summary>
public VirtualConsole(TextReader input, bool isInputRedirected,
TextWriter output, bool isOutputRedirected,
TextWriter error, bool isErrorRedirected)
TextWriter error, bool isErrorRedirected,
CancellationToken cancellationToken = default)
{
Input = input.GuardNotNull(nameof(input));
Input = input;
IsInputRedirected = isInputRedirected;
Output = output.GuardNotNull(nameof(output));
Output = output;
IsOutputRedirected = isOutputRedirected;
Error = error.GuardNotNull(nameof(error));
Error = error;
IsErrorRedirected = isErrorRedirected;
_cancellationToken = cancellationToken;
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole(TextReader input, TextWriter output, TextWriter error)
: this(input, true, output, true, error, true)
public VirtualConsole(TextReader input, TextWriter output, TextWriter error,
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).
/// Input stream (stdin) is replaced with a no-op stub.
/// </summary>
public VirtualConsole(TextWriter output, TextWriter error)
: this(TextReader.Null, output, error)
public VirtualConsole(TextWriter output, TextWriter error, CancellationToken cancellationToken = default)
: this(TextReader.Null, output, error, cancellationToken)
{
}
@@ -71,8 +76,8 @@ namespace CliFx.Services
/// Initializes an instance of <see cref="VirtualConsole"/> using output stream (stdout).
/// Input stream (stdin) and error stream (stderr) are replaced with no-op stubs.
/// </summary>
public VirtualConsole(TextWriter output)
: this(output, TextWriter.Null)
public VirtualConsole(TextWriter output, CancellationToken cancellationToken = default)
: this(output, TextWriter.Null, cancellationToken)
{
}
@@ -82,5 +87,8 @@ namespace CliFx.Services
ForegroundColor = ConsoleColor.Gray;
BackgroundColor = ConsoleColor.Black;
}
/// <inheritdoc />
public CancellationToken GetCancellationToken() => _cancellationToken;
}
}

View File

@@ -1,10 +1,9 @@
# CliFx
[![Build](https://img.shields.io/appveyor/ci/Tyrrrz/CliFx/master.svg)](https://ci.appveyor.com/project/Tyrrrz/CliFx/branch/master)
[![Tests](https://img.shields.io/appveyor/tests/Tyrrrz/CliFx/master.svg)](https://ci.appveyor.com/project/Tyrrrz/CliFx/branch/master/tests)
[![Coverage](https://img.shields.io/codecov/c/gh/Tyrrrz/CliFx/master.svg)](https://codecov.io/gh/Tyrrrz/CliFx)
[![NuGet](https://img.shields.io/nuget/v/CliFx.svg)](https://nuget.org/packages/CliFx)
[![NuGet](https://img.shields.io/nuget/dt/CliFx.svg)](https://nuget.org/packages/CliFx)
[![Build](https://github.com/Tyrrrz/CliFx/workflows/CI/badge.svg?branch=master)](https://github.com/Tyrrrz/CliFx/actions)
[![Coverage](https://codecov.io/gh/Tyrrrz/CliFx/branch/master/graph/badge.svg)](https://codecov.io/gh/Tyrrrz/CliFx)
[![Version](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)
[![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)
@@ -15,7 +14,6 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
## Download
- [NuGet](https://nuget.org/packages/CliFx): `dotnet add package CliFx`
- [Continuous integration](https://ci.appveyor.com/project/Tyrrrz/CliFx)
## 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
- Handles options of various types, including custom types
- Supports multi-level command hierarchies
- Allows cancellation
- Generates contextual help text
- Prints errors and routes exit codes on exceptions
- Highly testable and easy to debug
- Targets .NET Framework 4.5+ and .NET Standard 2.0+
- No external dependencies
## Screenshots
![help screen](.screenshots/help.png)
## 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.
@@ -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`.
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:
@@ -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.
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
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
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
CliFx has the smallest performance overhead compared to other command line parsers and frameworks.
Below you can see a table comparing execution times of a simple command across different libraries.
Here's how CliFx's execution overhead compares to that of other libraries.
```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
Frequency=3125008 Hz, Resolution=319.9992 ns, Timer=TSC
Frequency=3125011 Hz, Resolution=319.9989 ns, Timer=TSC
.NET Core SDK=2.2.401
[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
@@ -404,10 +458,12 @@ Job=Core Runtime=Core
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|------------------------------------- |----------:|----------:|----------:|------:|--------:|-----:|
| CliFx | 39.47 us | 0.7490 us | 0.9198 us | 1.00 | 0.00 | 1 |
| System.CommandLine | 153.98 us | 0.7112 us | 0.6652 us | 3.90 | 0.09 | 2 |
| McMaster.Extensions.CommandLineUtils | 180.36 us | 3.5893 us | 6.7416 us | 4.59 | 0.16 | 3 |
| PowerArgs | 427.54 us | 6.9006 us | 6.4548 us | 10.82 | 0.26 | 4 |
| CliFx | 31.29 us | 0.6147 us | 0.7774 us | 1.00 | 0.00 | 2 |
| System.CommandLine | 184.44 us | 3.4993 us | 4.0297 us | 5.90 | 0.21 | 4 |
| McMaster.Extensions.CommandLineUtils | 165.50 us | 1.4805 us | 1.3124 us | 5.33 | 0.13 | 3 |
| 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

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