mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70bfe0bf91 | ||
|
|
9690c380d3 | ||
|
|
85caa275ae | ||
|
|
32026e59c0 | ||
|
|
486ccb9685 | ||
|
|
7b766f70f3 | ||
|
|
f73e96488f | ||
|
|
af63fa5a1f | ||
|
|
e8f53c9463 | ||
|
|
9564cd5d30 | ||
|
|
ed458c3980 | ||
|
|
25538f99db | ||
|
|
36436e7a4b | ||
|
|
a6070332c9 | ||
|
|
25cbfdb4b8 | ||
|
|
d1b5107c2c | ||
|
|
03873d63cd | ||
|
|
89aba39964 | ||
|
|
ab57a103d1 | ||
|
|
d0b2ebc061 | ||
|
|
857257ca73 | ||
|
|
3587155c7e | ||
|
|
ae05e0db96 | ||
|
|
41c0493e66 | ||
|
|
43a304bb26 | ||
|
|
cd3892bf83 | ||
|
|
3f7c02342d | ||
|
|
c65cdf465e | ||
|
|
b5d67ecf24 | ||
|
|
a94b2296e1 | ||
|
|
fa05e4df3f | ||
|
|
b70b25076e | ||
|
|
0662f341e6 | ||
|
|
80bf477f3b | ||
|
|
e4a502d9d6 | ||
|
|
13b15b98ed | ||
|
|
80465e0e51 | ||
|
|
9a1ce7e7e5 | ||
|
|
b45da64664 | ||
|
|
df01dc055e | ||
|
|
31dd24d189 | ||
|
|
2a76dfe1c8 | ||
|
|
59ee2e34d8 |
27
.github/workflows/CD.yml
vendored
Normal file
27
.github/workflows/CD.yml
vendored
Normal 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
22
.github/workflows/CI.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -143,6 +143,7 @@ _TeamCity*
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
.ncrunchsolution
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
|
||||
BIN
.screenshots/help.png
Normal file
BIN
.screenshots/help.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
|
||||
20
CliFx.Benchmarks/Commands/CliprCommand.cs
Normal file
20
CliFx.Benchmarks/Commands/CliprCommand.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"})
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net46</TargetFramework>
|
||||
<Version>1.2.3.4</Version>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command]
|
||||
public class GreeterCommand : ICommand
|
||||
{
|
||||
[CommandOption("target", 't', Description = "Greeting target.")]
|
||||
public string Target { get; set; } = "world";
|
||||
|
||||
[CommandOption('e', Description = "Whether the greeting should be exclaimed.")]
|
||||
public bool IsExclaimed { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer.Append("Hello").Append(' ').Append(Target);
|
||||
|
||||
if (IsExclaimed)
|
||||
buffer.Append('!');
|
||||
|
||||
console.Output.WriteLine(buffer.ToString());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command("log", Description = "Calculate the logarithm of a value.")]
|
||||
public class LogCommand : ICommand
|
||||
{
|
||||
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
||||
public double Value { get; set; }
|
||||
|
||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||
public double Base { get; set; } = 10;
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var result = Math.Log(Value, Base);
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
{
|
||||
[Command("sum", Description = "Calculate the sum of all input values.")]
|
||||
public class SumCommand : ICommand
|
||||
{
|
||||
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
|
||||
public IReadOnlyList<double> Values { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var result = Values.Sum();
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Dummy
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static Task<int> Main(string[] args)
|
||||
{
|
||||
// Set culture to invariant to maintain consistent format because we rely on it in tests
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||
|
||||
return new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.UseDescription("Dummy program used for E2E tests.")
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
CliFx.Tests/CliApplicationBuilderTests.cs
Normal file
51
CliFx.Tests/CliApplicationBuilderTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.IO;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.Stubs;
|
||||
using CliFx.Tests.TestCommands;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CliApplicationBuilderTests
|
||||
{
|
||||
// Make sure all builder methods work
|
||||
[Test]
|
||||
public void All_Smoke_Test()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new CliApplicationBuilder();
|
||||
|
||||
// Act
|
||||
builder
|
||||
.AddCommand(typeof(HelloWorldDefaultCommand))
|
||||
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
|
||||
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
|
||||
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode()
|
||||
.AllowPreviewMode()
|
||||
.UseTitle("test")
|
||||
.UseExecutableName("test")
|
||||
.UseVersionText("test")
|
||||
.UseDescription("test")
|
||||
.UseConsole(new VirtualConsole(TextWriter.Null))
|
||||
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!)
|
||||
.UseCommandOptionInputConverter(new CommandOptionInputConverter())
|
||||
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
|
||||
.Build();
|
||||
}
|
||||
|
||||
// Make sure builder can produce an application with no parameters specified
|
||||
[Test]
|
||||
public void Build_Test()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new CliApplicationBuilder();
|
||||
|
||||
// Act
|
||||
builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CliApplicationTests
|
||||
{
|
||||
[Command]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine("DefaultCommand executed.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
private class NamedCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine("NamedCommand executed.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Negative
|
||||
public partial class CliApplicationTests
|
||||
{
|
||||
[Command("faulty1")]
|
||||
private class FaultyCommand1 : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => throw new CommandException(150);
|
||||
}
|
||||
|
||||
[Command("faulty2")]
|
||||
private class FaultyCommand2 : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => throw new CommandException("FaultyCommand2 error message.", 150);
|
||||
}
|
||||
|
||||
[Command("faulty3")]
|
||||
private class FaultyCommand3 : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => throw new Exception("FaultyCommand3 error message.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,123 @@
|
||||
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 FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using CliFx.Tests.Stubs;
|
||||
using CliFx.Tests.TestCommands;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class CliApplicationTests
|
||||
public class CliApplicationTests
|
||||
{
|
||||
private const string TestVersionText = "v1.0";
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new string[0],
|
||||
"DefaultCommand executed."
|
||||
"Hello world."
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"cmd"},
|
||||
"NamedCommand executed."
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_HelpAndVersion_RunAsync()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new string[0]
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
|
||||
"foo bar"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {"-h"}
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
|
||||
"one, two, three"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {"--help"}
|
||||
new[] {typeof(DivideCommand)},
|
||||
new[] {"div", "-D", "24", "-d", "8"},
|
||||
"3"
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {"--version"}
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new[] {"--version"},
|
||||
TestVersionText
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new string[0]
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"--version"},
|
||||
TestVersionText
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"cmd", "-h"}
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new[] {"-h"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand1)},
|
||||
new[] {"faulty1", "-h"}
|
||||
new[] {typeof(HelloWorldDefaultCommand)},
|
||||
new[] {"--help"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand2)},
|
||||
new[] {"faulty2", "-h"}
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new string[0],
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand3)},
|
||||
new[] {"faulty3", "-h"}
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"-h"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"--help"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "-h"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"exc", "-h"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-h"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"[preview]"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"exc", "[preview]"},
|
||||
null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"concat", "[preview]", "-o", "value"},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,97 +125,136 @@ namespace CliFx.Tests
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new Type[0],
|
||||
new string[0]
|
||||
new string[0],
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {"non-existing"}
|
||||
new[] {typeof(ConcatCommand)},
|
||||
new[] {"non-existing"},
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand1)},
|
||||
new[] {"faulty1"}
|
||||
new[] {typeof(ExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand2)},
|
||||
new[] {"faulty2"}
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand3)},
|
||||
new[] {"faulty3"}
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc"},
|
||||
null, null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-m", "foo bar"},
|
||||
"foo bar", null
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(CommandExceptionCommand)},
|
||||
new[] {"exc", "-m", "foo bar", "-c", "666"},
|
||||
"foo bar", 666
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
||||
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, string expectedStdOut)
|
||||
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
|
||||
string? expectedStdOut = null)
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
{
|
||||
var console = new VirtualConsole(stdout);
|
||||
await using var stdoutStream = new StringWriter();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
var console = new VirtualConsole(stdoutStream);
|
||||
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments);
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseVersionText(TestVersionText)
|
||||
.UseConsole(console)
|
||||
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdout.ToString().Trim().Should().Be(expectedStdOut);
|
||||
}
|
||||
}
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments);
|
||||
var stdOut = stdoutStream.ToString().Trim();
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_HelpAndVersion_RunAsync))]
|
||||
public async Task RunAsync_HelpAndVersion_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
{
|
||||
var console = new VirtualConsole(stdout);
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdout.ToString().Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
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)
|
||||
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
|
||||
string? expectedStdErr = null, int? expectedExitCode = null)
|
||||
{
|
||||
// Arrange
|
||||
using (var stderr = new StringWriter())
|
||||
{
|
||||
var console = new VirtualConsole(TextWriter.Null, stderr);
|
||||
await using var stderrStream = new StringWriter();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
var console = new VirtualConsole(TextWriter.Null, stderrStream);
|
||||
var environmentVariablesProvider = new EnvironmentVariablesProviderStub();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments);
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommands(commandTypes)
|
||||
.UseVersionText(TestVersionText)
|
||||
.UseEnvironmentVariablesProvider(environmentVariablesProvider)
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(commandLineArguments);
|
||||
var stderr = stderrStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
if (expectedExitCode != null)
|
||||
exitCode.Should().Be(expectedExitCode);
|
||||
else
|
||||
exitCode.Should().NotBe(0);
|
||||
stderr.ToString().Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
if (expectedStdErr != null)
|
||||
stderr.Should().Be(expectedStdErr);
|
||||
else
|
||||
stderr.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_Cancellation_Test()
|
||||
{
|
||||
// Arrange
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
await using var stdoutStream = new StringWriter();
|
||||
|
||||
var console = new VirtualConsole(stdoutStream, cancellationTokenSource.Token);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CancellableCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
var args = new[] {"cancel"};
|
||||
|
||||
// Act
|
||||
var runTask = application.RunAsync(args);
|
||||
cancellationTokenSource.Cancel();
|
||||
var exitCode = await runTask.ConfigureAwait(false);
|
||||
var stdOut = stdoutStream.ToString().Trim();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(-2146233029);
|
||||
stdOut.Should().Be("Printed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
<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="CliWrap" Version="2.3.1" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="2.7.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" />
|
||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandFactoryTests
|
||||
{
|
||||
[Command]
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandInitializerTests
|
||||
{
|
||||
[Command]
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
[CommandOption("int", 'i', IsRequired = true)]
|
||||
public int IntOption { get; set; } = 24;
|
||||
|
||||
[CommandOption("str", 's')]
|
||||
public string StringOption { get; set; } = "foo bar";
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class CommandInitializerTests
|
||||
{
|
||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new TestCommand(),
|
||||
GetCommandSchema(typeof(TestCommand)),
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("int", "13")
|
||||
}),
|
||||
new TestCommand {IntOption = 13}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new TestCommand(),
|
||||
GetCommandSchema(typeof(TestCommand)),
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("int", "13"),
|
||||
new CommandOptionInput("str", "hello world")
|
||||
}),
|
||||
new TestCommand {IntOption = 13, StringOption = "hello world"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new TestCommand(),
|
||||
GetCommandSchema(typeof(TestCommand)),
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("i", "13")
|
||||
}),
|
||||
new TestCommand {IntOption = 13}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new TestCommand(),
|
||||
GetCommandSchema(typeof(TestCommand)),
|
||||
CommandInput.Empty
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new TestCommand(),
|
||||
GetCommandSchema(typeof(TestCommand)),
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("str", "hello world")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
||||
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput, ICommand expectedCommand)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act
|
||||
initializer.InitializeCommand(command, commandSchema, commandInput);
|
||||
|
||||
// Assert
|
||||
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
|
||||
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act & Assert
|
||||
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
|
||||
.Should().ThrowExactly<MissingCommandOptionInputException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandInputParserTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_ParseCommandInput()
|
||||
{
|
||||
yield return new TestCaseData(new string[0], CommandInput.Empty);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option1", "value1", "--option2", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option1", "value1"),
|
||||
new CommandOptionInput("option2", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value1", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option", "value1", "--option", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "-b", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", "value1"),
|
||||
new CommandOptionInput("b", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "value1", "-a", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--option1", "value1", "-b", "value2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option1", "value1"),
|
||||
new CommandOptionInput("b", "value2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("switch")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"--switch1", "--switch2"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("switch1"),
|
||||
new CommandOptionInput("switch2")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-s"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("s")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-a", "-b"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-ab"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"-ab", "value"},
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"command"},
|
||||
new CommandInput("command")
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"command", "--option", "value"},
|
||||
new CommandInput("command", new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"long", "command", "name"},
|
||||
new CommandInput("long command name")
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"long", "command", "name", "--option", "value"},
|
||||
new CommandInput("long command name", new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_ParseCommandInput))]
|
||||
public void ParseCommandInput_Test(IReadOnlyList<string> commandLineArguments, CommandInput expectedCommandInput)
|
||||
{
|
||||
// Arrange
|
||||
var parser = new CommandInputParser();
|
||||
|
||||
// Act
|
||||
var commandInput = parser.ParseCommandInput(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
commandInput.Should().BeEquivalentTo(expectedCommandInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandOptionInputConverterTests
|
||||
{
|
||||
private enum TestEnum
|
||||
{
|
||||
Value1,
|
||||
Value2,
|
||||
Value3
|
||||
}
|
||||
|
||||
private class TestStringConstructable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public TestStringConstructable(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestStringParseable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private TestStringParseable(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
|
||||
}
|
||||
|
||||
private class TestStringParseableWithFormatProvider
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private TestStringParseableWithFormatProvider(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
||||
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
|
||||
}
|
||||
}
|
||||
|
||||
// Negative
|
||||
public partial class CommandOptionInputConverterTests
|
||||
{
|
||||
private class NonStringParseable
|
||||
{
|
||||
public int Value { get; }
|
||||
|
||||
public NonStringParseable(int value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CommandSchemaResolverTests
|
||||
{
|
||||
[Command("cmd", Description = "NormalCommand1 description.")]
|
||||
private class NormalCommand1 : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a')]
|
||||
public int OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", IsRequired = true)]
|
||||
public string OptionB { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command(Description = "NormalCommand2 description.")]
|
||||
private class NormalCommand2 : ICommand
|
||||
{
|
||||
[CommandOption("option-c", Description = "OptionC description.")]
|
||||
public bool OptionC { get; set; }
|
||||
|
||||
[CommandOption("option-d", 'd')]
|
||||
public DateTimeOffset OptionD { get; set; }
|
||||
|
||||
public string NotAnOption { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// Negative
|
||||
public partial class CommandSchemaResolverTests
|
||||
{
|
||||
[Command("conflict")]
|
||||
private class ConflictingCommand1 : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command("conflict")]
|
||||
private class ConflictingCommand2 : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class InvalidCommand1
|
||||
{
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class InvalidCommand2 : ICommand
|
||||
{
|
||||
[CommandOption("conflict")]
|
||||
public string ConflictingOption1 { get; set; }
|
||||
|
||||
[CommandOption("conflict")]
|
||||
public string ConflictingOption2 { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class InvalidCommand3 : ICommand
|
||||
{
|
||||
[CommandOption('c')]
|
||||
public string ConflictingOption1 { get; set; }
|
||||
|
||||
[CommandOption('c')]
|
||||
public string ConflictingOption2 { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class CommandSchemaResolverTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NormalCommand1), typeof(NormalCommand2)},
|
||||
new[]
|
||||
{
|
||||
new CommandSchema(typeof(NormalCommand1), "cmd", "NormalCommand1 description.",
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionA)),
|
||||
"option-a", 'a', false, null),
|
||||
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionB)),
|
||||
"option-b", null, true, null)
|
||||
}),
|
||||
new CommandSchema(typeof(NormalCommand2), null, "NormalCommand2 description.",
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionC)),
|
||||
"option-c", null, false, "OptionC description."),
|
||||
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionD)),
|
||||
"option-d", 'd', false, null)
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
|
||||
{
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new Type[0]
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(ConflictingCommand1), typeof(ConflictingCommand2)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(InvalidCommand1)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(InvalidCommand2)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(InvalidCommand3)}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
|
||||
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
||||
{
|
||||
// Arrange
|
||||
var commandSchemaResolver = new CommandSchemaResolver();
|
||||
|
||||
// Act
|
||||
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
|
||||
|
||||
// Assert
|
||||
commandSchemas.Should().BeEquivalentTo(expectedCommandSchemas);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas_Negative))]
|
||||
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new CommandSchemaResolver();
|
||||
|
||||
// Act & Assert
|
||||
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
||||
.Should().ThrowExactly<InvalidCommandSchemaException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DelegateCommandFactoryTests
|
||||
{
|
||||
[Command]
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliWrap;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DummyTests
|
||||
{
|
||||
private static string DummyFilePath => typeof(Dummy.Program).Assembly.Location;
|
||||
|
||||
private static string DummyVersionText => typeof(Dummy.Program).Assembly.GetName().Version.ToString();
|
||||
|
||||
[Test]
|
||||
[TestCase("", "Hello world")]
|
||||
[TestCase("-t .NET", "Hello .NET")]
|
||||
[TestCase("-e", "Hello world!")]
|
||||
[TestCase("sum -v 1 2", "3")]
|
||||
[TestCase("sum -v 2.75 3.6 4.18", "10.53")]
|
||||
[TestCase("sum -v 4 -v 16", "20")]
|
||||
[TestCase("sum --values 2 5 --values 3", "10")]
|
||||
[TestCase("log -v 100", "2")]
|
||||
[TestCase("log --value 256 --base 2", "8")]
|
||||
public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput)
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.StandardOutput.Trim().Should().Be(expectedOutput);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("--version")]
|
||||
public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments)
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.StandardOutput.Trim().Should().Be(DummyVersionText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("--help")]
|
||||
[TestCase("-h")]
|
||||
[TestCase("sum -h")]
|
||||
[TestCase("sum --help")]
|
||||
[TestCase("log -h")]
|
||||
[TestCase("log --help")]
|
||||
public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments)
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = await Cli.Wrap(DummyFilePath)
|
||||
.SetArguments(arguments)
|
||||
.EnableExitCodeValidation()
|
||||
.EnableStandardErrorValidation()
|
||||
.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.StandardOutput.Trim().Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class HelpTextRendererTests
|
||||
{
|
||||
[Command(Description = "DefaultCommand description.")]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', Description = "OptionA description.")]
|
||||
public string OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", 'b', Description = "OptionB description.")]
|
||||
public string OptionB { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command("cmd", Description = "NamedCommand description.")]
|
||||
private class NamedCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-c", 'c', Description = "OptionC description.")]
|
||||
public string OptionC { get; set; }
|
||||
|
||||
[CommandOption("option-d", 'd', Description = "OptionD description.")]
|
||||
public string OptionD { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command("cmd sub", Description = "NamedSubCommand description.")]
|
||||
private class NamedSubCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-e", 'e', Description = "OptionE description.")]
|
||||
public string OptionE { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,21 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class CommandFactoryTests
|
||||
public class CommandFactoryTests
|
||||
{
|
||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||
{
|
||||
yield return new TestCaseData(GetCommandSchema(typeof(TestCommand)));
|
||||
yield return new TestCaseData(GetCommandSchema(typeof(HelloWorldDefaultCommand)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
174
CliFx.Tests/Services/CommandInitializerTests.cs
Normal file
174
CliFx.Tests/Services/CommandInitializerTests.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
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 CliFx.Tests.Stubs;
|
||||
using System.IO;
|
||||
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandInitializerTests
|
||||
{
|
||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||
new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single();
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
{
|
||||
new CommandOptionInput("dividend", "13"),
|
||||
new CommandOptionInput("divisor", "8")
|
||||
}),
|
||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
{
|
||||
new CommandOptionInput("dividend", "13"),
|
||||
new CommandOptionInput("d", "8")
|
||||
}),
|
||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
{
|
||||
new CommandOptionInput("D", "13"),
|
||||
new CommandOptionInput("d", "8")
|
||||
}),
|
||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
{
|
||||
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
|
||||
}),
|
||||
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
{
|
||||
new CommandOptionInput("i", new[] {"foo", "bar"}),
|
||||
new CommandOptionInput("s", " ")
|
||||
}),
|
||||
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}" }
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div")
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
{
|
||||
new CommandOptionInput("D", "13")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat")
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
{
|
||||
new CommandOptionInput("s", "_")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
||||
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput,
|
||||
ICommand expectedCommand)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act
|
||||
initializer.InitializeCommand(command, commandSchema, commandInput);
|
||||
|
||||
// Assert
|
||||
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
|
||||
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
||||
{
|
||||
// Arrange
|
||||
var initializer = new CommandInitializer();
|
||||
|
||||
// Act & Assert
|
||||
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
|
||||
.Should().ThrowExactly<CliFxException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
255
CliFx.Tests/Services/CommandInputParserTests.cs
Normal file
255
CliFx.Tests/Services/CommandInputParserTests.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.Stubs;
|
||||
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandInputParserTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_ParseCommandInput()
|
||||
{
|
||||
yield return new TestCaseData(new string[0], CommandInput.Empty, new EmptyEnvironmentVariablesProviderStub());
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "--option", "value" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", "value")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "--option", "value1", "--option", "value2" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "-a", "value" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", "value")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("option1", "value1"),
|
||||
new CommandOptionInput("b", "value2")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "--switch" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("switch")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "--switch1", "--switch2" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("switch1"),
|
||||
new CommandOptionInput("switch2")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "-s" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("s")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "-a", "-b" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "-ab" },
|
||||
new CommandInput(new[]
|
||||
{
|
||||
new CommandOptionInput("a"),
|
||||
new CommandOptionInput("b")
|
||||
}),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
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 CommandInput(null,
|
||||
new[] { "debug" },
|
||||
new CommandOptionInput[0]),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "[debug]", "[preview]" },
|
||||
new CommandInput(null,
|
||||
new[] { "debug", "preview" },
|
||||
new CommandOptionInput[0]),
|
||||
new EmptyEnvironmentVariablesProviderStub()
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] { "[debug]", "[preview]", "-o", "value" },
|
||||
new CommandInput(null,
|
||||
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")
|
||||
}),
|
||||
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, IEnvironmentVariablesProvider environmentVariablesProvider)
|
||||
{
|
||||
// Arrange
|
||||
var parser = new CommandInputParser(environmentVariablesProvider);
|
||||
|
||||
// Act
|
||||
var commandInput = parser.ParseCommandInput(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
commandInput.Should().BeEquivalentTo(expectedCommandInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ using System.Globalization;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.TestCustomTypes;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class CommandOptionInputConverterTests
|
||||
public class CommandOptionInputConverterTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
|
||||
{
|
||||
@@ -213,6 +214,12 @@ namespace CliFx.Tests
|
||||
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[]),
|
||||
@@ -269,15 +276,26 @@ namespace CliFx.Tests
|
||||
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(NonStringParseable)
|
||||
typeof(TestNonStringParseable)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_ConvertOptionInput))]
|
||||
public void ConvertOptionInput_Test(CommandOptionInput optionInput, Type targetType, object expectedConvertedValue)
|
||||
public void ConvertOptionInput_Test(CommandOptionInput optionInput, Type targetType,
|
||||
object expectedConvertedValue)
|
||||
{
|
||||
// Arrange
|
||||
var converter = new CommandOptionInputConverter();
|
||||
@@ -299,7 +317,7 @@ namespace CliFx.Tests
|
||||
|
||||
// Act & Assert
|
||||
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
|
||||
.Should().ThrowExactly<InvalidCommandOptionInputException>();
|
||||
.Should().ThrowExactly<CliFxException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
CliFx.Tests/Services/CommandSchemaResolverTests.cs
Normal file
116
CliFx.Tests/Services/CommandSchemaResolverTests.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.TestCommands;
|
||||
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandSchemaResolverTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
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.", null),
|
||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
|
||||
"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),
|
||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.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[]
|
||||
{
|
||||
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
|
||||
{
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new Type[0]
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(NonImplementedCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(NonAnnotatedCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateOptionNamesCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(DuplicateOptionShortNamesCommand)}
|
||||
});
|
||||
|
||||
yield return new TestCaseData(new object[]
|
||||
{
|
||||
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
|
||||
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
|
||||
IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
||||
{
|
||||
// Arrange
|
||||
var commandSchemaResolver = new CommandSchemaResolver();
|
||||
|
||||
// Act
|
||||
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
|
||||
|
||||
// Assert
|
||||
commandSchemas.Should().BeEquivalentTo(expectedCommandSchemas);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas_Negative))]
|
||||
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new CommandSchemaResolver();
|
||||
|
||||
// Act & Assert
|
||||
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
||||
.Should().ThrowExactly<CliFxException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class DelegateCommandFactoryTests
|
||||
public class DelegateCommandFactoryTests
|
||||
{
|
||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
||||
@@ -17,8 +18,8 @@ namespace CliFx.Tests
|
||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)),
|
||||
GetCommandSchema(typeof(TestCommand))
|
||||
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type!)!),
|
||||
GetCommandSchema(typeof(HelloWorldDefaultCommand))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using CliFx.Tests.TestCommands;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class HelpTextRendererTests
|
||||
public class HelpTextRendererTests
|
||||
{
|
||||
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
|
||||
{
|
||||
@@ -27,11 +28,13 @@ namespace CliFx.Tests
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
CreateHelpTextSource(
|
||||
new[] {typeof(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
|
||||
typeof(DefaultCommand)),
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
typeof(HelpDefaultCommand)),
|
||||
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"HelpDefaultCommand description.",
|
||||
"Usage",
|
||||
"[command]", "[options]",
|
||||
"Options",
|
||||
@@ -40,20 +43,20 @@ namespace CliFx.Tests
|
||||
"-h|--help", "Shows help text.",
|
||||
"--version", "Shows version information.",
|
||||
"Commands",
|
||||
"cmd", "NamedCommand description.",
|
||||
"cmd", "HelpNamedCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
CreateHelpTextSource(
|
||||
new[] {typeof(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
|
||||
typeof(NamedCommand)),
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
typeof(HelpNamedCommand)),
|
||||
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"NamedCommand description.",
|
||||
"HelpNamedCommand description.",
|
||||
"Usage",
|
||||
"cmd", "[command]", "[options]",
|
||||
"Options",
|
||||
@@ -61,20 +64,20 @@ namespace CliFx.Tests
|
||||
"-d|--option-d", "OptionD description.",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"sub", "NamedSubCommand description.",
|
||||
"sub", "HelpSubCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
CreateHelpTextSource(
|
||||
new[] {typeof(DefaultCommand), typeof(NamedCommand), typeof(NamedSubCommand)},
|
||||
typeof(NamedSubCommand)),
|
||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
||||
typeof(HelpSubCommand)),
|
||||
|
||||
new[]
|
||||
{
|
||||
"Description",
|
||||
"NamedSubCommand description.",
|
||||
"HelpSubCommand description.",
|
||||
"Usage",
|
||||
"cmd sub", "[options]",
|
||||
"Options",
|
||||
@@ -86,20 +89,20 @@ namespace CliFx.Tests
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_RenderHelpText))]
|
||||
public void RenderHelpText_Test(HelpTextSource source, IReadOnlyList<string> expectedSubstrings)
|
||||
public void RenderHelpText_Test(HelpTextSource source,
|
||||
IReadOnlyList<string> expectedSubstrings)
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
{
|
||||
var renderer = new HelpTextRenderer();
|
||||
var console = new VirtualConsole(stdout);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
CliFx.Tests/Services/SystemConsoleTests.cs
Normal file
41
CliFx.Tests/Services/SystemConsoleTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public class SystemConsoleTests
|
||||
{
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Reset console color so it doesn't carry on into next tests
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
// Make sure console correctly wraps around System.Console
|
||||
[Test]
|
||||
public void All_Smoke_Test()
|
||||
{
|
||||
// Arrange
|
||||
var console = new SystemConsole();
|
||||
|
||||
// Act
|
||||
console.ResetColor();
|
||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
||||
|
||||
// Assert
|
||||
console.Input.Should().BeSameAs(Console.In);
|
||||
console.IsInputRedirected.Should().Be(Console.IsInputRedirected);
|
||||
console.Output.Should().BeSameAs(Console.Out);
|
||||
console.IsOutputRedirected.Should().Be(Console.IsOutputRedirected);
|
||||
console.Error.Should().BeSameAs(Console.Error);
|
||||
console.IsErrorRedirected.Should().Be(Console.IsErrorRedirected);
|
||||
console.ForegroundColor.Should().Be(Console.ForegroundColor);
|
||||
console.BackgroundColor.Should().Be(Console.BackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
CliFx.Tests/Services/VirtualConsoleTests.cs
Normal file
42
CliFx.Tests/Services/VirtualConsoleTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests.Services
|
||||
{
|
||||
[TestFixture]
|
||||
public class VirtualConsoleTests
|
||||
{
|
||||
// Make sure console uses specified streams and doesn't leak to System.Console
|
||||
[Test]
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
CliFx.Tests/Stubs/EmptyEnvironmentVariablesProviderStub.cs
Normal file
10
CliFx.Tests/Stubs/EmptyEnvironmentVariablesProviderStub.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/Stubs/EnvironmentVariablesProviderStub.cs
Normal file
18
CliFx.Tests/Stubs/EnvironmentVariablesProviderStub.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
22
CliFx.Tests/TestCommands/CancellableCommand.cs
Normal file
22
CliFx.Tests/TestCommands/CancellableCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
19
CliFx.Tests/TestCommands/CommandExceptionCommand.cs
Normal file
19
CliFx.Tests/TestCommands/CommandExceptionCommand.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("exc")]
|
||||
public class CommandExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("code", 'c')]
|
||||
public int ExitCode { get; set; } = 1337;
|
||||
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
|
||||
}
|
||||
}
|
||||
23
CliFx.Tests/TestCommands/ConcatCommand.cs
Normal file
23
CliFx.Tests/TestCommands/ConcatCommand.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("concat", Description = "Concatenate strings.")]
|
||||
public class ConcatCommand : ICommand
|
||||
{
|
||||
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
|
||||
public IReadOnlyList<string> Inputs { get; set; }
|
||||
|
||||
[CommandOption('s', Description = "String separator.")]
|
||||
public string Separator { get; set; } = "";
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(string.Join(Separator, Inputs));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
CliFx.Tests/TestCommands/DivideCommand.cs
Normal file
25
CliFx.Tests/TestCommands/DivideCommand.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("div", Description = "Divide one number by another.")]
|
||||
public class DivideCommand : ICommand
|
||||
{
|
||||
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
|
||||
public double Dividend { get; set; }
|
||||
|
||||
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
|
||||
public double Divisor { get; set; }
|
||||
|
||||
// This property should be ignored by resolver
|
||||
public bool NotAnOption { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(Dividend / Divisor);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs
Normal file
18
CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateOptionNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption("fruits")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
[CommandOption("fruits")]
|
||||
public string? Oranges { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs
Normal file
18
CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class DuplicateOptionShortNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption('f')]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
[CommandOption('f')]
|
||||
public string? Oranges { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
15
CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs
Normal file
15
CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
16
CliFx.Tests/TestCommands/ExceptionCommand.cs
Normal file
16
CliFx.Tests/TestCommands/ExceptionCommand.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("exc")]
|
||||
public class ExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => throw new Exception(Message);
|
||||
}
|
||||
}
|
||||
16
CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs
Normal file
16
CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class HelloWorldDefaultCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine("Hello world.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/TestCommands/HelpDefaultCommand.cs
Normal file
18
CliFx.Tests/TestCommands/HelpDefaultCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command(Description = "HelpDefaultCommand description.")]
|
||||
public class HelpDefaultCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', Description = "OptionA description.")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", 'b', Description = "OptionB description.")]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/TestCommands/HelpNamedCommand.cs
Normal file
18
CliFx.Tests/TestCommands/HelpNamedCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("cmd", Description = "HelpNamedCommand description.")]
|
||||
public class HelpNamedCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-c", 'c', Description = "OptionC description.")]
|
||||
public string? OptionC { get; set; }
|
||||
|
||||
[CommandOption("option-d", 'd', Description = "OptionD description.")]
|
||||
public string? OptionD { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
15
CliFx.Tests/TestCommands/HelpSubCommand.cs
Normal file
15
CliFx.Tests/TestCommands/HelpSubCommand.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command("cmd sub", Description = "HelpSubCommand description.")]
|
||||
public class HelpSubCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-e", 'e', Description = "OptionE description.")]
|
||||
public string? OptionE { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
10
CliFx.Tests/TestCommands/NonAnnotatedCommand.cs
Normal file
10
CliFx.Tests/TestCommands/NonAnnotatedCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
public class NonAnnotatedCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests/TestCommands/NonImplementedCommand.cs
Normal file
9
CliFx.Tests/TestCommands/NonImplementedCommand.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.TestCommands
|
||||
{
|
||||
[Command]
|
||||
public class NonImplementedCommand
|
||||
{
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests/TestCustomTypes/TestEnum.cs
Normal file
9
CliFx.Tests/TestCustomTypes/TestEnum.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CliFx.Tests.TestCustomTypes
|
||||
{
|
||||
public enum TestEnum
|
||||
{
|
||||
Value1,
|
||||
Value2,
|
||||
Value3
|
||||
}
|
||||
}
|
||||
12
CliFx.Tests/TestCustomTypes/TestNonStringParseable.cs
Normal file
12
CliFx.Tests/TestCustomTypes/TestNonStringParseable.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CliFx.Tests.TestCustomTypes
|
||||
{
|
||||
public class TestNonStringParseable
|
||||
{
|
||||
public int Value { get; }
|
||||
|
||||
public TestNonStringParseable(int value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
CliFx.Tests/TestCustomTypes/TestStringConstructable.cs
Normal file
12
CliFx.Tests/TestCustomTypes/TestStringConstructable.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CliFx.Tests.TestCustomTypes
|
||||
{
|
||||
public class TestStringConstructable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public TestStringConstructable(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
CliFx.Tests/TestCustomTypes/TestStringParseable.cs
Normal file
14
CliFx.Tests/TestCustomTypes/TestStringParseable.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace CliFx.Tests.TestCustomTypes
|
||||
{
|
||||
public class TestStringParseable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private TestStringParseable(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Tests.TestCustomTypes
|
||||
{
|
||||
public class TestStringParseableWithFormatProvider
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private TestStringParseableWithFormatProvider(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
||||
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
|
||||
}
|
||||
}
|
||||
55
CliFx.Tests/Utilities/ProgressTickerTests.cs
Normal file
55
CliFx.Tests/Utilities/ProgressTickerTests.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CliFx.Services;
|
||||
using CliFx.Utilities;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CliFx.Tests.Utilities
|
||||
{
|
||||
[TestFixture]
|
||||
public class ProgressTickerTests
|
||||
{
|
||||
[Test]
|
||||
public void Report_Test()
|
||||
{
|
||||
// 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();
|
||||
|
||||
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
|
||||
var progressStringValues = progressValues.Select(p => p.ToString("P2", formatProvider)).ToArray();
|
||||
|
||||
// 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();
|
||||
|
||||
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
|
||||
|
||||
// Act
|
||||
foreach (var progress in progressValues)
|
||||
ticker.Report(progress);
|
||||
|
||||
// Assert
|
||||
stdout.ToString().Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
CliFx.sln
16
CliFx.sln
@@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Changelog.md = Changelog.md
|
||||
@@ -18,7 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -54,18 +52,6 @@ Global
|
||||
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Exceptions;
|
||||
@@ -31,113 +32,197 @@ 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)
|
||||
{
|
||||
// Debug mode is enabled if it's allowed in the application and it was requested via corresponding directive
|
||||
var isDebugMode = _configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified();
|
||||
|
||||
// If not in debug mode, pass execution to the next handler
|
||||
if (!isDebugMode)
|
||||
return null;
|
||||
|
||||
// Inform user which process they need to attach debugger to
|
||||
_console.WithForegroundColor(ConsoleColor.Green,
|
||||
() => _console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
|
||||
|
||||
// Wait until debugger is attached
|
||||
while (!Debugger.IsAttached)
|
||||
await Task.Delay(100);
|
||||
|
||||
// Debug directive never short-circuits
|
||||
return null;
|
||||
}
|
||||
|
||||
private int? HandlePreviewDirective(CommandInput commandInput)
|
||||
{
|
||||
// Preview mode is enabled if it's allowed in the application and it was requested via corresponding directive
|
||||
var isPreviewMode = _configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified();
|
||||
|
||||
// If not in preview mode, pass execution to the next handler
|
||||
if (!isPreviewMode)
|
||||
return null;
|
||||
|
||||
// Render command name
|
||||
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
|
||||
_console.Output.WriteLine();
|
||||
|
||||
// Render directives
|
||||
_console.Output.WriteLine("Directives:");
|
||||
foreach (var directive in commandInput.Directives)
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.WriteLine(directive);
|
||||
}
|
||||
|
||||
// Margin
|
||||
_console.Output.WriteLine();
|
||||
|
||||
// Render options
|
||||
_console.Output.WriteLine("Options:");
|
||||
foreach (var option in commandInput.Options)
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.WriteLine(option);
|
||||
}
|
||||
|
||||
// Short-circuit with exit code 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int? HandleVersionOption(CommandInput commandInput)
|
||||
{
|
||||
// Version should be rendered if it was requested on a default command
|
||||
var shouldRenderVersion = !commandInput.IsCommandSpecified() && commandInput.IsVersionOptionSpecified();
|
||||
|
||||
// If shouldn't render version, pass execution to the next handler
|
||||
if (!shouldRenderVersion)
|
||||
return null;
|
||||
|
||||
// Render version text
|
||||
_console.Output.WriteLine(_metadata.VersionText);
|
||||
|
||||
// Short-circuit with exit code 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int? HandleHelpOption(CommandInput commandInput,
|
||||
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;
|
||||
|
||||
// If shouldn't render help, pass execution to the next handler
|
||||
if (!shouldRenderHelp)
|
||||
return null;
|
||||
|
||||
// Keep track whether there was an error in the input
|
||||
var isError = false;
|
||||
|
||||
// If target command isn't defined, find its contextual replacement
|
||||
if (targetCommandSchema == null)
|
||||
{
|
||||
// If command was specified, inform the user that it's not defined
|
||||
if (commandInput.IsCommandSpecified())
|
||||
{
|
||||
_console.WithForegroundColor(ConsoleColor.Red,
|
||||
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
|
||||
|
||||
isError = true;
|
||||
}
|
||||
|
||||
// Replace target command with closest parent of specified command
|
||||
targetCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
|
||||
|
||||
// If there's no parent, replace with stub default command
|
||||
if (targetCommandSchema == null)
|
||||
{
|
||||
targetCommandSchema = CommandSchema.StubDefaultCommand;
|
||||
availableCommandSchemas = availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Build help text source
|
||||
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
|
||||
|
||||
// Render help text
|
||||
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
||||
|
||||
// Short-circuit with appropriate exit code
|
||||
return isError ? -1 : 0;
|
||||
}
|
||||
|
||||
private async Task<int> HandleCommandExecutionAsync(CommandInput commandInput, CommandSchema targetCommandSchema)
|
||||
{
|
||||
// Create an instance of the command
|
||||
var command = _commandFactory.CreateCommand(targetCommandSchema);
|
||||
|
||||
// Populate command with options according to its schema
|
||||
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
|
||||
|
||||
// Execute command
|
||||
await command.ExecuteAsync(_console);
|
||||
|
||||
// Finish the chain with exit code 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
|
||||
|
||||
try
|
||||
{
|
||||
// Get schemas for all available command types
|
||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||
|
||||
// Parse command input from arguments
|
||||
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments);
|
||||
|
||||
// Get schemas for all available command types
|
||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||
|
||||
// Find command schema matching the name specified in the input
|
||||
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
|
||||
|
||||
// Handle cases where requested command is not defined
|
||||
if (targetCommandSchema == null)
|
||||
{
|
||||
var isError = false;
|
||||
|
||||
// If specified a command - show error
|
||||
if (commandInput.IsCommandSpecified())
|
||||
{
|
||||
isError = true;
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Red,
|
||||
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
|
||||
}
|
||||
|
||||
// Get parent command schema
|
||||
var parentCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
|
||||
|
||||
// Show help for parent command if it's defined
|
||||
if (parentCommandSchema != null)
|
||||
{
|
||||
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, parentCommandSchema);
|
||||
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
||||
}
|
||||
// Otherwise show help for a stub default command
|
||||
else
|
||||
{
|
||||
var helpTextSource = new HelpTextSource(_metadata,
|
||||
availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray(),
|
||||
CommandSchema.StubDefaultCommand);
|
||||
|
||||
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
||||
}
|
||||
|
||||
return isError ? -1 : 0;
|
||||
}
|
||||
|
||||
// Show version if it was requested without specifying a command
|
||||
if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
|
||||
{
|
||||
_console.Output.WriteLine(_metadata.VersionText);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Show help if it was requested
|
||||
if (commandInput.IsHelpRequested())
|
||||
{
|
||||
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
|
||||
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create an instance of the command
|
||||
var command = _commandFactory.CreateCommand(targetCommandSchema);
|
||||
|
||||
// Populate command with options according to its schema
|
||||
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
|
||||
|
||||
// Execute command
|
||||
await command.ExecuteAsync(_console);
|
||||
|
||||
return 0;
|
||||
// Chain handlers until the first one that produces an exit code
|
||||
return
|
||||
await HandleDebugDirectiveAsync(commandInput) ??
|
||||
HandlePreviewDirective(commandInput) ??
|
||||
HandleVersionOption(commandInput) ??
|
||||
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
|
||||
await HandleCommandExecutionAsync(commandInput, targetCommandSchema!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We want to catch exceptions in order to print errors and return correct exit codes.
|
||||
// Also, by doing this we get rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
|
||||
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
|
||||
|
||||
// In case we catch a CliFx-specific exception, we want to just show the error message, not the stack trace.
|
||||
// Stack trace isn't very useful to the user if the exception is not really coming from their code.
|
||||
// Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
|
||||
if (!string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException))
|
||||
{
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.Message));
|
||||
}
|
||||
else
|
||||
{
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex));
|
||||
}
|
||||
|
||||
// CommandException is the same, but it also lets users specify exit code so we want to return that instead of default.
|
||||
|
||||
var message = ex is CliFxException && !ex.Message.IsNullOrWhiteSpace() ? ex.Message : ex.ToString();
|
||||
var exitCode = ex is CommandException commandEx ? commandEx.ExitCode : ex.HResult;
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(message));
|
||||
|
||||
return exitCode;
|
||||
// Return exit code if it was specified via CommandException
|
||||
if (ex is CommandException commandException)
|
||||
{
|
||||
return commandException.ExitCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ex.HResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
@@ -16,18 +17,20 @@ namespace CliFx
|
||||
{
|
||||
private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
|
||||
|
||||
private string _title;
|
||||
private string _executableName;
|
||||
private string _versionText;
|
||||
private string _description;
|
||||
private IConsole _console;
|
||||
private ICommandFactory _commandFactory;
|
||||
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 ICommandOptionInputConverter? _commandOptionInputConverter;
|
||||
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AddCommand(Type commandType)
|
||||
{
|
||||
commandType.GuardNotNull(nameof(commandType));
|
||||
|
||||
_commandTypes.Add(commandType);
|
||||
|
||||
return this;
|
||||
@@ -36,9 +39,10 @@ namespace CliFx
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
|
||||
{
|
||||
commandAssembly.GuardNotNull(nameof(commandAssembly));
|
||||
|
||||
var commandTypes = commandAssembly.ExportedTypes.Where(t => t.Implements(typeof(ICommand)));
|
||||
var commandTypes = commandAssembly.ExportedTypes
|
||||
.Where(t => t.Implements(typeof(ICommand)))
|
||||
.Where(t => t.IsDefined(typeof(CommandAttribute)))
|
||||
.Where(t => !t.IsAbstract && !t.IsInterface);
|
||||
|
||||
foreach (var commandType in commandTypes)
|
||||
AddCommand(commandType);
|
||||
@@ -46,112 +50,95 @@ namespace CliFx
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AllowDebugMode(bool isAllowed = true)
|
||||
{
|
||||
_isDebugModeAllowed = isAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
|
||||
{
|
||||
_isPreviewModeAllowed = isAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
private void SetFallbackValues()
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
|
||||
{
|
||||
if (_title.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Entry assembly is null in tests
|
||||
UseTitle(EntryAssembly?.GetName().Name ?? "App");
|
||||
}
|
||||
_commandOptionInputConverter = converter;
|
||||
return this;
|
||||
}
|
||||
|
||||
if (_executableName.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Entry assembly is null in tests
|
||||
var entryAssemblyLocation = EntryAssembly?.Location;
|
||||
|
||||
// Set different executable name depending on location
|
||||
if (!entryAssemblyLocation.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Prepend 'dotnet' to assembly file name if the entry assembly is a dll file (extension needs to be kept)
|
||||
if (string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UseExecutableName("dotnet " + Path.GetFileName(entryAssemblyLocation));
|
||||
}
|
||||
// Otherwise just use assembly file name without extension
|
||||
else
|
||||
{
|
||||
UseExecutableName(Path.GetFileNameWithoutExtension(entryAssemblyLocation));
|
||||
}
|
||||
}
|
||||
// If location is null then just use a stub
|
||||
else
|
||||
{
|
||||
UseExecutableName("app");
|
||||
}
|
||||
}
|
||||
|
||||
if (_versionText.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Entry assembly is null in tests
|
||||
UseVersionText(EntryAssembly?.GetName().Version.ToString() ?? "1.0");
|
||||
}
|
||||
|
||||
if (_console == null)
|
||||
{
|
||||
UseConsole(new SystemConsole());
|
||||
}
|
||||
|
||||
if (_commandFactory == null)
|
||||
{
|
||||
UseCommandFactory(new CommandFactory());
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder UseEnvironmentVariablesProvider(IEnvironmentVariablesProvider environmentVariablesProvider)
|
||||
{
|
||||
_environmentVariablesProvider = environmentVariablesProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplication Build()
|
||||
{
|
||||
// Use defaults for required parameters that were not configured
|
||||
SetFallbackValues();
|
||||
_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());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +146,25 @@ namespace CliFx
|
||||
{
|
||||
private static readonly Lazy<Assembly> LazyEntryAssembly = new Lazy<Assembly>(Assembly.GetEntryAssembly);
|
||||
|
||||
// Entry assembly is null in tests
|
||||
private static Assembly EntryAssembly => LazyEntryAssembly.Value;
|
||||
|
||||
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name ?? "";
|
||||
|
||||
private static string GetDefaultExecutableName()
|
||||
{
|
||||
var entryAssemblyLocation = EntryAssembly?.Location;
|
||||
|
||||
// If it's a .dll assembly, prepend 'dotnet' and keep the file extension
|
||||
if (string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "dotnet " + Path.GetFileName(entryAssemblyLocation);
|
||||
}
|
||||
|
||||
// Otherwise just use assembly file name without extension
|
||||
return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
||||
}
|
||||
|
||||
private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version}" : "";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.0.2</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>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="all" />
|
||||
<PackageReference Include="Nullable" Version="1.1.1" PrivateAssets="all" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../favicon.png" Pack="True" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,12 +5,12 @@ namespace CliFx.Exceptions
|
||||
/// <summary>
|
||||
/// Domain exception thrown within CliFx.
|
||||
/// </summary>
|
||||
public abstract class CliFxException : Exception
|
||||
public class CliFxException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||
/// </summary>
|
||||
protected 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>
|
||||
protected CliFxException(string message, Exception innerException)
|
||||
public CliFxException(string? message, Exception? innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
@@ -8,7 +7,7 @@ namespace CliFx.Exceptions
|
||||
/// Use this exception if you want to report an error that occured during execution of a command.
|
||||
/// This exception also allows specifying exit code which will be returned to the calling process.
|
||||
/// </summary>
|
||||
public class CommandException : CliFxException
|
||||
public class CommandException : Exception
|
||||
{
|
||||
private const int DefaultExitCode = -100;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a command option can't be converted to target type specified in its schema.
|
||||
/// </summary>
|
||||
public class InvalidCommandOptionInputException : CliFxException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="InvalidCommandOptionInputException"/>.
|
||||
/// </summary>
|
||||
public InvalidCommandOptionInputException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="InvalidCommandOptionInputException"/>.
|
||||
/// </summary>
|
||||
public InvalidCommandOptionInputException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a command schema fails validation.
|
||||
/// </summary>
|
||||
public class InvalidCommandSchemaException : CliFxException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
|
||||
/// </summary>
|
||||
public InvalidCommandSchemaException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
|
||||
/// </summary>
|
||||
public InvalidCommandSchemaException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a required command option was not set.
|
||||
/// </summary>
|
||||
public class MissingCommandOptionInputException : CliFxException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="MissingCommandOptionInputException"/>.
|
||||
/// </summary>
|
||||
public MissingCommandOptionInputException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="MissingCommandOptionInputException"/>.
|
||||
/// </summary>
|
||||
public MissingCommandOptionInputException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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));
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,16 @@ namespace CliFx
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether debug mode (enabled with [debug] directive) is allowed in the application.
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AllowDebugMode(bool isAllowed = true);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true);
|
||||
|
||||
/// <summary>
|
||||
/// Sets application title, which appears in the help text.
|
||||
/// </summary>
|
||||
@@ -37,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"/>.
|
||||
@@ -49,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.
|
||||
|
||||
@@ -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);
|
||||
@@ -26,11 +24,6 @@ namespace CliFx.Internal
|
||||
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
||||
builder.Length > 0 ? builder.Append(value) : builder;
|
||||
|
||||
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
||||
dic.TryGetValue(key, out var result) ? result : default;
|
||||
|
||||
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
|
||||
|
||||
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value)
|
||||
{
|
||||
foreach (var i in source)
|
||||
@@ -41,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);
|
||||
|
||||
@@ -51,7 +49,7 @@ namespace CliFx.Internal
|
||||
|
||||
return type.GetInterfaces()
|
||||
.Select(GetEnumerableUnderlyingType)
|
||||
.ExceptNull()
|
||||
.Where(t => t != default)
|
||||
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
||||
.FirstOrDefault();
|
||||
}
|
||||
@@ -65,5 +63,8 @@ namespace CliFx.Internal
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static bool IsCollection(this Type type) =>
|
||||
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
@@ -10,16 +9,29 @@ namespace CliFx.Models
|
||||
public class ApplicationConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Command types defined in the application.
|
||||
/// Command types defined in this application.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> CommandTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether debug mode is allowed in this application.
|
||||
/// </summary>
|
||||
public bool IsDebugModeAllowed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether preview mode is allowed in this application.
|
||||
/// </summary>
|
||||
public bool IsPreviewModeAllowed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
|
||||
/// </summary>
|
||||
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes)
|
||||
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes,
|
||||
bool isDebugModeAllowed, bool isPreviewModeAllowed)
|
||||
{
|
||||
CommandTypes = commandTypes.GuardNotNull(nameof(commandTypes));
|
||||
CommandTypes = commandTypes;
|
||||
IsDebugModeAllowed = isDebugModeAllowed;
|
||||
IsPreviewModeAllowed = isPreviewModeAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,57 @@ 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.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Directives { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specified options.
|
||||
/// </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<CommandOptionInput> options)
|
||||
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Directives = directives;
|
||||
Options = options;
|
||||
EnvironmentVariables = environmentVariables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
|
||||
: this(commandName, directives, options, EmptyEnvironmentVariables)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
|
||||
: this(commandName, EmptyDirectives, options, environmentVariables)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
|
||||
: this(commandName, EmptyDirectives, options)
|
||||
{
|
||||
CommandName = commandName; // can be null
|
||||
Options = options.GuardNotNull(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,16 +77,8 @@ namespace CliFx.Models
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string commandName)
|
||||
: this(commandName, new CommandOptionInput[0])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput()
|
||||
: this(null, new CommandOptionInput[0])
|
||||
public CommandInput(string? commandName)
|
||||
: this(commandName, EmptyOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -58,9 +87,15 @@ namespace CliFx.Models
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
if (!CommandName.IsNullOrWhiteSpace())
|
||||
if (!string.IsNullOrWhiteSpace(CommandName))
|
||||
buffer.Append(CommandName);
|
||||
|
||||
foreach (var directive in Directives)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(directive);
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
@@ -73,9 +108,13 @@ namespace CliFx.Models
|
||||
|
||||
public partial class CommandInput
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
public static CommandInput Empty { get; } = new CommandInput();
|
||||
public static CommandInput Empty { get; } = new CommandInput(EmptyOptions);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace CliFx.Models
|
||||
/// <summary>
|
||||
/// Parsed option from command line input.
|
||||
/// </summary>
|
||||
public class CommandOptionInput
|
||||
public partial class CommandOptionInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Specified option alias.
|
||||
@@ -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>
|
||||
@@ -40,7 +40,7 @@ namespace CliFx.Models
|
||||
/// Initializes an instance of <see cref="CommandOptionInput"/>.
|
||||
/// </summary>
|
||||
public CommandOptionInput(string alias)
|
||||
: this(alias, new string[0])
|
||||
: this(alias, EmptyValues)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -70,4 +70,9 @@ namespace CliFx.Models
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CommandOptionInput
|
||||
{
|
||||
private static readonly IReadOnlyList<string> EmptyValues = new string[0];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -71,17 +64,12 @@ namespace CliFx.Models
|
||||
|
||||
return matchesByName || matchesByShortName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an option that matches specified alias, or null if not found.
|
||||
/// </summary>
|
||||
public static CommandOptionSchema FindByAlias(this IReadOnlyList<CommandOptionSchema> optionSchemas, string alias)
|
||||
{
|
||||
optionSchemas.GuardNotNull(nameof(optionSchemas));
|
||||
alias.GuardNotNull(nameof(alias));
|
||||
|
||||
return optionSchemas.FirstOrDefault(o => o.MatchesAlias(alias));
|
||||
}
|
||||
/// <summary>
|
||||
/// Finds an option input that matches the option schema specified, or null if not found.
|
||||
/// </summary>
|
||||
public static CommandOptionInput? FindByOptionSchema(this IReadOnlyList<CommandOptionInput> optionInputs, CommandOptionSchema optionSchema) =>
|
||||
optionInputs.FirstOrDefault(o => optionSchema.MatchesAlias(o.Alias));
|
||||
|
||||
/// <summary>
|
||||
/// 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,43 +90,41 @@ 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 help was requested in the input.
|
||||
/// Gets whether debug directive was specified in the input.
|
||||
/// </summary>
|
||||
public static bool IsHelpRequested(this CommandInput commandInput)
|
||||
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.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether version information was requested in the input.
|
||||
/// Gets whether version option was specified in the input.
|
||||
/// </summary>
|
||||
public static bool IsVersionRequested(this CommandInput commandInput)
|
||||
public static bool IsVersionOptionSpecified(this CommandInput commandInput)
|
||||
{
|
||||
commandInput.GuardNotNull(nameof(commandInput));
|
||||
|
||||
var firstOption = commandInput.Options.FirstOrDefault();
|
||||
|
||||
return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this command is the default command, i.e. without a name.
|
||||
/// </summary>
|
||||
public static bool IsDefault(this CommandSchema commandSchema)
|
||||
{
|
||||
commandSchema.GuardNotNull(nameof(commandSchema));
|
||||
return commandSchema.Name.IsNullOrWhiteSpace();
|
||||
}
|
||||
public static bool IsDefault(this CommandSchema commandSchema) => string.IsNullOrWhiteSpace(commandSchema.Name);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -61,7 +84,7 @@ namespace CliFx.Services
|
||||
if (unsetRequiredOptions.Any())
|
||||
{
|
||||
var unsetRequiredOptionNames = unsetRequiredOptions.Select(o => o.GetAliases().FirstOrDefault()).JoinToString(", ");
|
||||
throw new MissingCommandOptionInputException($"One or more required options were not set: {unsetRequiredOptionNames}.");
|
||||
throw new CliFxException($"One or more required options were not set: {unsetRequiredOptionNames}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,32 @@ 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>>();
|
||||
|
||||
// Option aliases and values are parsed in pairs so we need to keep track of last alias
|
||||
var lastOptionAlias = "";
|
||||
|
||||
foreach (var commandLineArgument in commandLineArguments)
|
||||
@@ -34,7 +52,7 @@ namespace CliFx.Services
|
||||
optionsDic[lastOptionAlias] = new List<string>();
|
||||
}
|
||||
|
||||
// Encountered short option name or multiple thereof
|
||||
// Encountered short option name or multiple short option names
|
||||
else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Handle stacked options
|
||||
@@ -48,15 +66,26 @@ namespace CliFx.Services
|
||||
}
|
||||
}
|
||||
|
||||
// Encountered command name or part thereof
|
||||
else if (lastOptionAlias.IsNullOrWhiteSpace())
|
||||
// Encountered directive or (part of) command name
|
||||
else if (string.IsNullOrWhiteSpace(lastOptionAlias))
|
||||
{
|
||||
commandNameBuilder.AppendIfNotEmpty(' ');
|
||||
commandNameBuilder.Append(commandLineArgument);
|
||||
if (commandLineArgument.StartsWith("[", StringComparison.OrdinalIgnoreCase) &&
|
||||
commandLineArgument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract directive
|
||||
var directive = commandLineArgument.Substring(1, commandLineArgument.Length - 2);
|
||||
|
||||
directives.Add(directive);
|
||||
}
|
||||
else
|
||||
{
|
||||
commandNameBuilder.AppendIfNotEmpty(' ');
|
||||
commandNameBuilder.Append(commandLineArgument);
|
||||
}
|
||||
}
|
||||
|
||||
// Encountered option value
|
||||
else if (!lastOptionAlias.IsNullOrWhiteSpace())
|
||||
else if (!string.IsNullOrWhiteSpace(lastOptionAlias))
|
||||
{
|
||||
optionsDic[lastOptionAlias].Add(commandLineArgument);
|
||||
}
|
||||
@@ -65,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, options);
|
||||
var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables();
|
||||
|
||||
return new CommandInput(commandName, directives, options, environmentVariables);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 InvalidCommandOptionInputException($"Can't convert value [{value}] to type [{targetType}].");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidCommandOptionInputException($"Can't convert value [{value}] to type [{targetType}].", 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 new InvalidCommandOptionInputException(
|
||||
$"Can't convert sequence of values [{optionInput.Values.JoinToString(", ")}] to type [{targetType}].");
|
||||
// Throw if we can't find a way to convert the values
|
||||
throw new CliFxException(
|
||||
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
|
||||
$"to type [{targetType}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,114 +14,109 @@ namespace CliFx.Services
|
||||
/// </summary>
|
||||
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||
{
|
||||
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
|
||||
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
||||
{
|
||||
var attribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||
var result = new List<CommandOptionSchema>();
|
||||
|
||||
if (attribute == null)
|
||||
return null;
|
||||
foreach (var property in commandType.GetProperties())
|
||||
{
|
||||
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
|
||||
|
||||
return new CommandOptionSchema(optionProperty,
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.IsRequired,
|
||||
attribute.Description);
|
||||
}
|
||||
// If an attribute is not set, then it's not an option so we just skip it
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
private CommandSchema GetCommandSchema(Type commandType)
|
||||
{
|
||||
// Attribute is optional for commands in order to reduce runtime rule complexity
|
||||
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
// Build option schema
|
||||
var optionSchema = new CommandOptionSchema(property,
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.IsRequired,
|
||||
attribute.Description,
|
||||
attribute.EnvironmentVariableName);
|
||||
|
||||
var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
|
||||
// Make sure there are no other options with the same name
|
||||
var existingOptionWithSameName = result
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.FirstOrDefault(o => string.Equals(o.Name, optionSchema.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return new CommandSchema(commandType,
|
||||
attribute?.Name,
|
||||
attribute?.Description,
|
||||
options);
|
||||
if (existingOptionWithSameName != null)
|
||||
{
|
||||
throw new CliFxException(
|
||||
$"Command type [{commandType}] has options defined with the same name: " +
|
||||
$"[{existingOptionWithSameName.Property}] and [{optionSchema.Property}].");
|
||||
}
|
||||
|
||||
// Make sure there are no other options with the same short name
|
||||
var existingOptionWithSameShortName = result
|
||||
.Where(o => o.ShortName != null)
|
||||
.FirstOrDefault(o => o.ShortName == optionSchema.ShortName);
|
||||
|
||||
if (existingOptionWithSameShortName != null)
|
||||
{
|
||||
throw new CliFxException(
|
||||
$"Command type [{commandType}] has options defined with the same short name: " +
|
||||
$"[{existingOptionWithSameShortName.Property}] and [{optionSchema.Property}].");
|
||||
}
|
||||
|
||||
// Add schema to list
|
||||
result.Add(optionSchema);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
commandTypes.GuardNotNull(nameof(commandTypes));
|
||||
|
||||
// Get command schemas
|
||||
var commandSchemas = commandTypes.Select(GetCommandSchema).ToArray();
|
||||
|
||||
// Throw if there are no commands defined
|
||||
if (!commandSchemas.Any())
|
||||
// Make sure there's at least one command defined
|
||||
if (!commandTypes.Any())
|
||||
{
|
||||
throw new InvalidCommandSchemaException("There are no commands defined.");
|
||||
throw new CliFxException("There are no commands defined.");
|
||||
}
|
||||
|
||||
// Throw if there are multiple commands with the same name
|
||||
var nonUniqueCommandNames = commandSchemas
|
||||
.Select(c => c.Name)
|
||||
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var result = new List<CommandSchema>();
|
||||
|
||||
foreach (var commandName in nonUniqueCommandNames)
|
||||
foreach (var commandType in commandTypes)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace()
|
||||
? $"There are multiple commands defined with name [{commandName}]."
|
||||
: "There are multiple default commands defined.");
|
||||
}
|
||||
|
||||
// Throw if there are commands that don't implement ICommand
|
||||
var nonImplementedCommandNames = commandSchemas
|
||||
.Where(c => !c.Type.Implements(typeof(ICommand)))
|
||||
.Select(c => c.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var commandName in nonImplementedCommandNames)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace()
|
||||
? $"Command [{commandName}] doesn't implement ICommand."
|
||||
: "Default command doesn't implement ICommand.");
|
||||
}
|
||||
|
||||
// Throw if there are multiple options with the same name inside the same command
|
||||
foreach (var commandSchema in commandSchemas)
|
||||
{
|
||||
var nonUniqueOptionNames = commandSchema.Options
|
||||
.Where(o => !o.Name.IsNullOrWhiteSpace())
|
||||
.Select(o => o.Name)
|
||||
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var optionName in nonUniqueOptionNames)
|
||||
// Make sure command type implements ICommand.
|
||||
if (!commandType.Implements(typeof(ICommand)))
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||
? $"There are multiple options defined with name [{optionName}] on command [{commandSchema.Name}]."
|
||||
: $"There are multiple options defined with name [{optionName}] on default command.");
|
||||
throw new CliFxException($"Command type [{commandType}] must implement {typeof(ICommand)}.");
|
||||
}
|
||||
|
||||
var nonUniqueOptionShortNames = commandSchema.Options
|
||||
.Where(o => o.ShortName != null)
|
||||
.Select(o => o.ShortName.Value)
|
||||
.GroupBy(i => i)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
// Get attribute
|
||||
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
foreach (var optionShortName in nonUniqueOptionShortNames)
|
||||
// Make sure attribute is set
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||
? $"There are multiple options defined with short name [{optionShortName}] on command [{commandSchema.Name}]."
|
||||
: $"There are multiple options defined with short name [{optionShortName}] on default command.");
|
||||
throw new CliFxException($"Command type [{commandType}] must be annotated with [{typeof(CommandAttribute)}].");
|
||||
}
|
||||
|
||||
// Get option schemas
|
||||
var optionSchemas = GetCommandOptionSchemas(commandType);
|
||||
|
||||
// Build command schema
|
||||
var commandSchema = new CommandSchema(commandType,
|
||||
attribute.Name,
|
||||
attribute.Description,
|
||||
optionSchemas);
|
||||
|
||||
// Make sure there are no other commands with the same name
|
||||
var existingCommandWithSameName = result
|
||||
.FirstOrDefault(c => string.Equals(c.Name, commandSchema.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingCommandWithSameName != null)
|
||||
{
|
||||
throw new CliFxException(
|
||||
$"Command type [{existingCommandWithSameName.Type}] has the same name as another command type [{commandType}].");
|
||||
}
|
||||
|
||||
// Add schema to list
|
||||
result.Add(commandSchema);
|
||||
}
|
||||
|
||||
return commandSchemas;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
27
CliFx/Services/EnvironmentVariablesParser.cs
Normal file
27
CliFx/Services/EnvironmentVariablesParser.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
CliFx/Services/EnvironmentVariablesProvider.cs
Normal file
36
CliFx/Services/EnvironmentVariablesProvider.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -169,8 +166,14 @@ namespace CliFx.Services
|
||||
// Header
|
||||
RenderHeader("Options");
|
||||
|
||||
// Order options and append built-in options
|
||||
var allOptionSchemas = source.TargetCommandSchema.Options
|
||||
.OrderByDescending(o => o.IsRequired)
|
||||
.Concat(builtInOptionSchemas)
|
||||
.ToArray();
|
||||
|
||||
// Options
|
||||
foreach (var optionSchema in source.TargetCommandSchema.Options.Concat(builtInOptionSchemas))
|
||||
foreach (var optionSchema in allOptionSchemas)
|
||||
{
|
||||
// Is required
|
||||
if (optionSchema.IsRequired)
|
||||
@@ -189,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);
|
||||
@@ -225,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);
|
||||
@@ -248,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);
|
||||
@@ -279,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);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user