mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Improve exceptions
This commit is contained in:
@@ -4,11 +4,16 @@ using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ApplicationSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ApplicationSpecs(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public void Application_can_be_created_with_a_default_configuration()
|
||||
{
|
||||
@@ -52,7 +57,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = Array.Empty<Type>();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -62,7 +68,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonImplementedCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -72,7 +79,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonAnnotatedCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -82,7 +90,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -92,7 +101,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -102,7 +112,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -112,7 +123,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -122,7 +134,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -132,7 +145,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(EmptyOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -142,7 +156,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -152,7 +167,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -162,7 +178,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -172,7 +189,8 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -5,11 +5,16 @@ using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ArgumentBindingSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ArgumentBindingSpecs(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public void Property_of_type_object_is_bound_directly_from_the_argument_value()
|
||||
{
|
||||
@@ -943,7 +948,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -957,7 +963,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -996,7 +1003,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1010,7 +1018,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1024,7 +1033,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1038,7 +1048,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1052,7 +1063,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1067,7 +1079,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1084,7 +1097,8 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
using CliFx.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DependencyInjectionSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public DependencyInjectionSpecs(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public void Default_type_activator_can_initialize_a_command_if_it_has_a_parameterless_constructor()
|
||||
{
|
||||
@@ -26,8 +31,8 @@ namespace CliFx.Tests
|
||||
var activator = new DefaultTypeActivator();
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() =>
|
||||
activator.CreateInstance(typeof(WithDependenciesCommand)));
|
||||
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(typeof(WithDependenciesCommand)));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -48,11 +53,11 @@ namespace CliFx.Tests
|
||||
public void Delegate_type_activator_throws_if_the_underlying_function_returns_null()
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DelegateTypeActivator(_ => null);
|
||||
var activator = new DelegateTypeActivator(_ => null!);
|
||||
|
||||
// Act & assert
|
||||
Assert.Throws<CliFxException>(() =>
|
||||
activator.CreateInstance(typeof(WithDependenciesCommand)));
|
||||
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(typeof(WithDependenciesCommand)));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,10 +173,10 @@ namespace CliFx
|
||||
|
||||
public partial class CliApplicationBuilder
|
||||
{
|
||||
private static readonly Lazy<Assembly> LazyEntryAssembly = new Lazy<Assembly>(Assembly.GetEntryAssembly);
|
||||
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 Assembly? EntryAssembly => LazyEntryAssembly.Value;
|
||||
|
||||
private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
|
||||
|
||||
@@ -184,14 +184,12 @@ namespace CliFx
|
||||
{
|
||||
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);
|
||||
}
|
||||
// The assembly can be an executable or a dll, depending on how it was packaged
|
||||
var isDll = string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Otherwise just use assembly file name without extension
|
||||
return Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
||||
return isDll
|
||||
? "dotnet " + Path.GetFileName(entryAssemblyLocation)
|
||||
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
||||
}
|
||||
|
||||
private static string? GetDefaultVersionText() =>
|
||||
|
||||
@@ -18,11 +18,7 @@ namespace CliFx
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.Append($"Failed to create an instance of {type.FullName}.").Append(" ")
|
||||
.AppendLine("The type must have a public parameter-less constructor in order to be instantiated by the default activator.")
|
||||
.Append($"To supply a custom activator (for example when using dependency injection), call {nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...).")
|
||||
.ToString(), ex);
|
||||
throw CliFxException.DefaultActivatorFailed(type, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ namespace CliFx
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateInstance(Type type) =>
|
||||
_func(type) ?? throw new CliFxException(new StringBuilder()
|
||||
.Append($"Failed to create an instance of type {type.FullName}, received <null> instead.").Append(" ")
|
||||
.Append("Make sure that the provided type activator was configured correctly.").Append(" ")
|
||||
.Append("If you are using a dependency container, make sure that this type is registered.")
|
||||
.ToString());
|
||||
_func(type) ?? throw CliFxException.DelegateActivatorReceivedNull(type);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -71,18 +68,14 @@ namespace CliFx.Domain
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
ITypeActivator activator)
|
||||
{
|
||||
var command = TryFindCommand(commandLineInput, out var argumentOffset);
|
||||
if (command == null)
|
||||
{
|
||||
throw new CliFxException(
|
||||
$"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.UnboundArguments)}].");
|
||||
}
|
||||
var command = TryFindCommand(commandLineInput, out var argumentOffset) ??
|
||||
throw CliFxException.CannotFindMatchingCommand(commandLineInput);
|
||||
|
||||
var parameterValues = argumentOffset == 0
|
||||
? commandLineInput.UnboundArguments.Select(a => a.Value).ToArray()
|
||||
: commandLineInput.UnboundArguments.Skip(argumentOffset).Select(a => a.Value).ToArray();
|
||||
var parameterInputs = argumentOffset == 0
|
||||
? commandLineInput.UnboundArguments.ToArray()
|
||||
: commandLineInput.UnboundArguments.Skip(argumentOffset).ToArray();
|
||||
|
||||
return command.CreateInstance(parameterValues, commandLineInput.Options, environmentVariables, activator);
|
||||
return command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator);
|
||||
}
|
||||
|
||||
public ICommand InitializeEntryPoint(
|
||||
@@ -106,28 +99,23 @@ namespace CliFx.Domain
|
||||
|
||||
if (duplicateOrderGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same order ({duplicateOrderGroup.Key}):")
|
||||
.AppendBulletList(duplicateOrderGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Parameters in a command must all have unique order.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandParametersDuplicateOrder(
|
||||
command,
|
||||
duplicateOrderGroup.Key,
|
||||
duplicateOrderGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Parameters
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.Name))
|
||||
.GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(a => a.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same name ({duplicateNameGroup.Key}):")
|
||||
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Parameters in a command must all have unique names.").Append(" ")
|
||||
.Append("Comparison is NOT case-sensitive.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandParametersDuplicateName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var nonScalarParameters = command.Parameters
|
||||
@@ -136,13 +124,9 @@ namespace CliFx.Domain
|
||||
|
||||
if (nonScalarParameters.Length > 1)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command [{command.Type.FullName}] contains two or more parameters of an enumerable type:")
|
||||
.AppendBulletList(nonScalarParameters.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.AppendLine("There can only be one parameter of an enumerable type in a command.")
|
||||
.Append("Note, the string type is not considered enumerable in this context.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandParametersTooManyNonScalar(
|
||||
command,
|
||||
nonScalarParameters);
|
||||
}
|
||||
|
||||
var nonLastNonScalarParameter = command.Parameters
|
||||
@@ -152,92 +136,74 @@ namespace CliFx.Domain
|
||||
|
||||
if (nonLastNonScalarParameter != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains a parameter of an enumerable type which doesn't appear last in order:")
|
||||
.AppendLine($"- {nonLastNonScalarParameter.Property.Name}")
|
||||
.AppendLine()
|
||||
.Append("Parameter of an enumerable type must always come last to avoid ambiguity.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandParametersNonLastNonScalar(
|
||||
command,
|
||||
nonLastNonScalarParameter);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOptions(CommandSchema command)
|
||||
{
|
||||
var emptyNameGroup = command.Options
|
||||
var noNameGroup = command.Options
|
||||
.Where(o => o.ShortName == null && string.IsNullOrWhiteSpace(o.Name))
|
||||
.ToArray();
|
||||
|
||||
if (emptyNameGroup.Any())
|
||||
if (noNameGroup.Any())
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains one or more options that have empty names:")
|
||||
.AppendBulletList(emptyNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Options in a command must all have at least a name or a short name.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandOptionsNoName(
|
||||
command,
|
||||
noNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var invalidNameGroup = command.Options
|
||||
var invalidLengthNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.Where(o => o.Name.Length <= 1)
|
||||
.Where(o => o.Name!.Length <= 1)
|
||||
.ToArray();
|
||||
|
||||
if (invalidNameGroup.Any())
|
||||
if (invalidLengthNameGroup.Any())
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains one or more options that have names that are too short:")
|
||||
.AppendBulletList(invalidNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Options in a command must all have names that are longer than a single character.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandOptionsInvalidLengthName(
|
||||
command,
|
||||
invalidLengthNameGroup);
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.GroupBy(o => o.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(o => o.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same name ({duplicateNameGroup.Key}):")
|
||||
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Options in a command must all have unique names.").Append(" ")
|
||||
.Append("Comparison is NOT case-sensitive.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandOptionsDuplicateName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateShortNameGroup = command.Options
|
||||
.Where(o => o.ShortName != null)
|
||||
.GroupBy(o => o.ShortName)
|
||||
.GroupBy(o => o.ShortName!.Value)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateShortNameGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same short name ({duplicateShortNameGroup.Key}):")
|
||||
.AppendBulletList(duplicateShortNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Options in a command must all have unique short names.").Append(" ")
|
||||
.Append("Comparison is case-sensitive.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandOptionsDuplicateShortName(
|
||||
command,
|
||||
duplicateShortNameGroup.Key,
|
||||
duplicateShortNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateEnvironmentVariableNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName))
|
||||
.GroupBy(o => o.EnvironmentVariableName, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(o => o.EnvironmentVariableName!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateEnvironmentVariableNameGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same environment variable name ({duplicateEnvironmentVariableNameGroup.Key}):")
|
||||
.AppendBulletList(duplicateEnvironmentVariableNameGroup.Select(o => o.Property.Name))
|
||||
.AppendLine()
|
||||
.Append("Options in a command must all have unique environment variable names.").Append(" ")
|
||||
.Append("Comparison is NOT case-sensitive.")
|
||||
.ToString());
|
||||
throw CliFxException.CommandOptionsDuplicateEnvironmentVariableName(
|
||||
command,
|
||||
duplicateEnvironmentVariableNameGroup.Key,
|
||||
duplicateEnvironmentVariableNameGroup.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +211,7 @@ namespace CliFx.Domain
|
||||
{
|
||||
if (!commands.Any())
|
||||
{
|
||||
throw new CliFxException("There are no commands configured for this application.");
|
||||
throw CliFxException.CommandsNotRegistered();
|
||||
}
|
||||
|
||||
var duplicateNameGroup = commands
|
||||
@@ -254,13 +220,12 @@ namespace CliFx.Domain
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Application contains two or more commands that have the same name ({duplicateNameGroup.Key}):")
|
||||
.AppendBulletList(duplicateNameGroup.Select(o => o.Type.FullName))
|
||||
.AppendLine()
|
||||
.Append("Commands must all have unique names. Likewise, there must not be more than one command without a name.").Append(" ")
|
||||
.Append("Comparison is NOT case-sensitive.")
|
||||
.ToString());
|
||||
if (!string.IsNullOrWhiteSpace(duplicateNameGroup.Key))
|
||||
throw CliFxException.CommandsDuplicateName(
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
|
||||
throw CliFxException.CommandsTooManyDefaults(duplicateNameGroup.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,17 +235,8 @@ namespace CliFx.Domain
|
||||
|
||||
foreach (var commandType in commandTypes)
|
||||
{
|
||||
var command = CommandSchema.TryResolve(commandType);
|
||||
if (command == null)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.Append($"Command {commandType.FullName} is not a valid command type.").Append(" ")
|
||||
.AppendLine("In order to be a valid command type it must:")
|
||||
.AppendLine($" - Be annotated with {typeof(CommandAttribute).FullName}")
|
||||
.AppendLine($" - Implement {typeof(ICommand).FullName}")
|
||||
.AppendLine(" - Not be an abstract class")
|
||||
.ToString());
|
||||
}
|
||||
var command = CommandSchema.TryResolve(commandType) ??
|
||||
throw CliFxException.InvalidCommandType(commandType);
|
||||
|
||||
ValidateParameters(command);
|
||||
ValidateOptions(command);
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
|
||||
@@ -15,7 +14,9 @@ namespace CliFx.Domain
|
||||
|
||||
public string? Description { get; }
|
||||
|
||||
public bool IsScalar => GetEnumerableArgumentUnderlyingType() == null;
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
|
||||
|
||||
protected CommandArgumentSchema(PropertyInfo property, string? description)
|
||||
{
|
||||
@@ -23,28 +24,85 @@ namespace CliFx.Domain
|
||||
Description = description;
|
||||
}
|
||||
|
||||
private Type? GetEnumerableArgumentUnderlyingType() =>
|
||||
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
||||
Property.PropertyType != typeof(string)
|
||||
? Property.PropertyType.GetEnumerableUnderlyingType()
|
||||
: null;
|
||||
|
||||
private object Convert(IReadOnlyList<string> values)
|
||||
private object? ConvertScalar(string? value, Type targetType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Primitive
|
||||
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
|
||||
if (primitiveConverter != null)
|
||||
return primitiveConverter(value);
|
||||
|
||||
// Enum
|
||||
if (targetType.IsEnum)
|
||||
return Enum.Parse(targetType, value, true);
|
||||
|
||||
// Nullable
|
||||
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
|
||||
if (nullableUnderlyingType != null)
|
||||
return !string.IsNullOrWhiteSpace(value)
|
||||
? ConvertScalar(value, nullableUnderlyingType)
|
||||
: null;
|
||||
|
||||
// String-constructable
|
||||
var stringConstructor = GetStringConstructor(targetType);
|
||||
if (stringConstructor != null)
|
||||
return stringConstructor.Invoke(new object[] {value!});
|
||||
|
||||
// String-parseable (with format provider)
|
||||
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
||||
if (parseMethodWithFormatProvider != null)
|
||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, ConversionFormatProvider});
|
||||
|
||||
// String-parseable (without format provider)
|
||||
var parseMethod = GetStaticParseMethod(targetType);
|
||||
if (parseMethod != null)
|
||||
return parseMethod.Invoke(null, new object[] {value!});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw CliFxException.CannotConvertToType(this, value, targetType, ex);
|
||||
}
|
||||
|
||||
throw CliFxException.CannotConvertToType(this, value, targetType);
|
||||
}
|
||||
|
||||
private object ConvertNonScalar(IReadOnlyList<string> values, Type targetEnumerableType, Type targetElementType)
|
||||
{
|
||||
var array = values
|
||||
.Select(v => ConvertScalar(v, targetElementType))
|
||||
.ToNonGenericArray(targetElementType);
|
||||
|
||||
var arrayType = array.GetType();
|
||||
|
||||
// Assignable from an array
|
||||
if (targetEnumerableType.IsAssignableFrom(arrayType))
|
||||
return array;
|
||||
|
||||
// Constructable from an array
|
||||
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
|
||||
if (arrayConstructor != null)
|
||||
return arrayConstructor.Invoke(new object[] {array});
|
||||
|
||||
throw CliFxException.CannotConvertNonScalar(this, values, targetEnumerableType);
|
||||
}
|
||||
|
||||
private object? Convert(IReadOnlyList<string> values)
|
||||
{
|
||||
var targetType = Property.PropertyType;
|
||||
var enumerableUnderlyingType = GetEnumerableArgumentUnderlyingType();
|
||||
var enumerableUnderlyingType = TryGetEnumerableArgumentUnderlyingType();
|
||||
|
||||
// Scalar
|
||||
if (enumerableUnderlyingType == null)
|
||||
{
|
||||
if (values.Count > 1)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Can't convert a sequence of values [{string.Join(", ", values)}] to type {targetType.FullName}.")
|
||||
.Append("Target type is not enumerable and can't accept more than one value.")
|
||||
.ToString());
|
||||
}
|
||||
|
||||
return ConvertScalar(values.SingleOrDefault(), targetType);
|
||||
return values.Count <= 1
|
||||
? ConvertScalar(values.SingleOrDefault(), targetType)
|
||||
: throw CliFxException.CannotConvertMultipleValuesToNonScalar(this, values);
|
||||
}
|
||||
// Non-scalar
|
||||
else
|
||||
@@ -64,8 +122,8 @@ namespace CliFx.Domain
|
||||
{
|
||||
private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture;
|
||||
|
||||
private static readonly IReadOnlyDictionary<Type, Func<string, object>> PrimitiveConverters =
|
||||
new Dictionary<Type, Func<string?, object>>
|
||||
private static readonly IReadOnlyDictionary<Type, Func<string?, object?>> PrimitiveConverters =
|
||||
new Dictionary<Type, Func<string?, object?>>
|
||||
{
|
||||
[typeof(object)] = v => v,
|
||||
[typeof(string)] = v => v,
|
||||
@@ -99,78 +157,5 @@ namespace CliFx.Domain
|
||||
type.GetMethod("Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null, new[] {typeof(string), typeof(IFormatProvider)}, null);
|
||||
|
||||
private static object ConvertScalar(string? value, Type targetType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Primitive
|
||||
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
|
||||
if (primitiveConverter != null)
|
||||
return primitiveConverter(value);
|
||||
|
||||
// Enum
|
||||
if (targetType.IsEnum)
|
||||
return Enum.Parse(targetType, value, true);
|
||||
|
||||
// Nullable
|
||||
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
|
||||
if (nullableUnderlyingType != null)
|
||||
return !string.IsNullOrWhiteSpace(value)
|
||||
? ConvertScalar(value, nullableUnderlyingType)
|
||||
: null;
|
||||
|
||||
// String-constructable
|
||||
var stringConstructor = GetStringConstructor(targetType);
|
||||
if (stringConstructor != null)
|
||||
return stringConstructor.Invoke(new object[] {value});
|
||||
|
||||
// String-parseable (with format provider)
|
||||
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
||||
if (parseMethodWithFormatProvider != null)
|
||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value, ConversionFormatProvider});
|
||||
|
||||
// String-parseable (without format provider)
|
||||
var parseMethod = GetStaticParseMethod(targetType);
|
||||
if (parseMethod != null)
|
||||
return parseMethod.Invoke(null, new object[] {value});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Failed to convert value '{value ?? "<null>"}' to type {targetType.FullName}.")
|
||||
.Append(ex.Message)
|
||||
.ToString(), ex);
|
||||
}
|
||||
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Can't convert value '{value ?? "<null>"}' to type {targetType.FullName}.")
|
||||
.Append("Target type is not supported by CliFx.")
|
||||
.ToString());
|
||||
}
|
||||
|
||||
private static object ConvertNonScalar(IReadOnlyList<string> values, Type targetEnumerableType, Type targetElementType)
|
||||
{
|
||||
var array = values
|
||||
.Select(v => ConvertScalar(v, targetElementType))
|
||||
.ToNonGenericArray(targetElementType);
|
||||
|
||||
var arrayType = array.GetType();
|
||||
|
||||
// Assignable from an array
|
||||
if (targetEnumerableType.IsAssignableFrom(arrayType))
|
||||
return array;
|
||||
|
||||
// Constructable from an array
|
||||
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
|
||||
if (arrayConstructor != null)
|
||||
return arrayConstructor.Invoke(new object[] {array});
|
||||
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine($"Can't convert a sequence of values [{string.Join(", ", values)}] to type {targetEnumerableType.FullName}.")
|
||||
.AppendLine($"Underlying element type is [{targetElementType.FullName}].")
|
||||
.Append("Target type must either be assignable from an array or have a public constructor that takes a single array argument.")
|
||||
.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ namespace CliFx.Domain
|
||||
{
|
||||
public string Alias { get; }
|
||||
|
||||
public string DisplayAlias =>
|
||||
Alias.Length > 1
|
||||
? $"--{Alias}"
|
||||
: $"-{Alias}";
|
||||
|
||||
public IReadOnlyList<string> Values { get; }
|
||||
|
||||
public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias);
|
||||
@@ -24,8 +29,7 @@ namespace CliFx.Domain
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer.Append(Alias.Length > 1 ? "--" : "-");
|
||||
buffer.Append(Alias);
|
||||
buffer.Append(DisplayAlias);
|
||||
|
||||
foreach (var value in Values)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -13,9 +12,9 @@ namespace CliFx.Domain
|
||||
|
||||
public char? ShortName { get; }
|
||||
|
||||
public string DisplayName => !string.IsNullOrWhiteSpace(Name)
|
||||
? Name
|
||||
: ShortName?.AsString()!;
|
||||
public override string DisplayName => !string.IsNullOrWhiteSpace(Name)
|
||||
? $"--{Name}"
|
||||
: $"-{ShortName}";
|
||||
|
||||
public string? EnvironmentVariableName { get; }
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace CliFx.Domain
|
||||
|
||||
public string? Name { get; }
|
||||
|
||||
public string DisplayName => !string.IsNullOrWhiteSpace(Name)
|
||||
public override string DisplayName =>
|
||||
!string.IsNullOrWhiteSpace(Name)
|
||||
? Name
|
||||
: Property.Name.ToLowerInvariant();
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace CliFx.Domain
|
||||
|
||||
public bool MatchesName(string? name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private void InjectParameters(ICommand command, IReadOnlyList<string> parameterInputs)
|
||||
private void InjectParameters(ICommand command, IReadOnlyList<CommandUnboundArgumentInput> parameterInputs)
|
||||
{
|
||||
// All inputs must be bound
|
||||
var remainingParameterInputs = parameterInputs.ToList();
|
||||
@@ -57,9 +57,9 @@ namespace CliFx.Domain
|
||||
|
||||
var scalarParameterInput = i < parameterInputs.Count
|
||||
? parameterInputs[i]
|
||||
: throw new CliFxException($"Missing value for parameter <{scalarParameter.DisplayName}>.");
|
||||
: throw CliFxException.ParameterNotSet(scalarParameter);
|
||||
|
||||
scalarParameter.Inject(command, scalarParameterInput);
|
||||
scalarParameter.Inject(command, scalarParameterInput.Value);
|
||||
remainingParameterInputs.Remove(scalarParameterInput);
|
||||
}
|
||||
|
||||
@@ -70,18 +70,16 @@ namespace CliFx.Domain
|
||||
|
||||
if (nonScalarParameter != null)
|
||||
{
|
||||
var nonScalarParameterInputs = parameterInputs.Skip(scalarParameters.Length).ToArray();
|
||||
nonScalarParameter.Inject(command, nonScalarParameterInputs);
|
||||
var nonScalarParameterValues = parameterInputs.Skip(scalarParameters.Length).Select(i => i.Value).ToArray();
|
||||
|
||||
nonScalarParameter.Inject(command, nonScalarParameterValues);
|
||||
remainingParameterInputs.Clear();
|
||||
}
|
||||
|
||||
// Ensure all inputs were bound
|
||||
if (remainingParameterInputs.Any())
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine("Unrecognized parameters provided:")
|
||||
.AppendBulletList(remainingParameterInputs)
|
||||
.ToString());
|
||||
throw CliFxException.UnrecognizedParametersProvided(remainingParameterInputs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,24 +134,18 @@ namespace CliFx.Domain
|
||||
// Ensure all required options were set
|
||||
if (unsetRequiredOptions.Any())
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine("Missing values for some of the required options:")
|
||||
.AppendBulletList(unsetRequiredOptions.Select(o => o.DisplayName))
|
||||
.ToString());
|
||||
throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions);
|
||||
}
|
||||
|
||||
// Ensure all inputs were bound
|
||||
if (remainingOptionInputs.Any())
|
||||
{
|
||||
throw new CliFxException(new StringBuilder()
|
||||
.AppendLine("Unrecognized options provided:")
|
||||
.AppendBulletList(remainingOptionInputs.Select(o => o.Alias).Distinct())
|
||||
.ToString());
|
||||
throw CliFxException.UnrecognizedOptionsProvided(remainingOptionInputs);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand CreateInstance(
|
||||
IReadOnlyList<string> parameterInputs,
|
||||
IReadOnlyList<CommandUnboundArgumentInput> parameterInputs,
|
||||
IReadOnlyList<CommandOptionInput> optionInputs,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
ITypeActivator activator)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Domain;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Domain exception thrown within CliFx.
|
||||
/// </summary>
|
||||
public class CliFxException : Exception
|
||||
public partial class CliFxException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||
@@ -23,4 +27,340 @@ namespace CliFx.Exceptions
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Mid-user-facing exceptions
|
||||
// Provide more diagnostic information here
|
||||
public partial class CliFxException
|
||||
{
|
||||
internal static CliFxException DefaultActivatorFailed(Type type, Exception? innerException = null)
|
||||
{
|
||||
var configureActivatorMethodName = $"{nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...)";
|
||||
|
||||
var message = $@"
|
||||
Failed to create an instance of type '{type.FullName}'.
|
||||
The type must have a public parameterless constructor in order to be instantiated by the default activator.
|
||||
|
||||
To fix this, either make sure this type has a public parameterless constructor, or configure a custom activator using {configureActivatorMethodName}.
|
||||
Refer to the readme to learn how to integrate a dependency container of your choice to act as a type activator.";
|
||||
|
||||
return new CliFxException(message.Trim(), innerException);
|
||||
}
|
||||
|
||||
internal static CliFxException DelegateActivatorReceivedNull(Type type)
|
||||
{
|
||||
var message = $@"
|
||||
Failed to create an instance of type '{type.FullName}', received <null> instead.
|
||||
|
||||
To fix this, ensure that the provided type activator was configured correctly, as it's not expected to return <null>.
|
||||
If you are using a dependency container, ensure this type is registered, because it may return <null> otherwise.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException InvalidCommandType(Type type)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{type.FullName}' is not a valid command type.
|
||||
|
||||
In order to be a valid command type, it must:
|
||||
- Not be an abstract class
|
||||
- Implement {typeof(ICommand).FullName}
|
||||
- Be annotated with {typeof(CommandAttribute).FullName}
|
||||
|
||||
To fix this, ensure that the command adheres to these constraints.
|
||||
If you're experiencing problems, please refer to readme for a quickstart example.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsNotRegistered()
|
||||
{
|
||||
var message = $@"
|
||||
There are no commands configured in the application.
|
||||
|
||||
To fix this, ensure that at least one command is added through one of the methods on {nameof(CliApplicationBuilder)}.
|
||||
If you're experiencing problems, please refer to readme for a quickstart example.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsTooManyDefaults(
|
||||
IReadOnlyList<CommandSchema> invalidCommands)
|
||||
{
|
||||
var message = $@"
|
||||
Application configuration is invalid because there are {invalidCommands.Count} default commands:
|
||||
{string.Join(Environment.NewLine, invalidCommands.Select(p => p.Type.FullName))}
|
||||
|
||||
There can only be one default command (i.e. command with no name) in an application.
|
||||
Other commands must have unique non-empty names that identify them.
|
||||
|
||||
To fix this, ensure that all extra commands have different names.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsDuplicateName(
|
||||
string name,
|
||||
IReadOnlyList<CommandSchema> invalidCommands)
|
||||
{
|
||||
var message = $@"
|
||||
Application configuration is invalid because there are {invalidCommands.Count} commands with the same name ('{name}'):
|
||||
{string.Join(Environment.NewLine, invalidCommands.Select(p => p.Type.FullName))}
|
||||
|
||||
Commands must have unique names, because that's what identifies them.
|
||||
Names are not case-sensitive.
|
||||
|
||||
To fix this, ensure that all commands have different names.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateOrder(
|
||||
CommandSchema command,
|
||||
int order,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same order ({order}):
|
||||
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))}
|
||||
|
||||
Parameters must have unique order, because that's what identifies them.
|
||||
|
||||
To fix this, ensure that all parameters have different order.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateName(
|
||||
CommandSchema command,
|
||||
string name,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same name ('{name}'):
|
||||
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))}
|
||||
|
||||
Parameters must have unique names to avoid potential confusion in the help text.
|
||||
Names are not case-sensitive.
|
||||
|
||||
To fix this, ensure that all parameters have different names.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersTooManyNonScalar(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} non-scalar parameters:
|
||||
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))}
|
||||
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object).
|
||||
Only one parameter in a command may be non-scalar and it must be the last one in order.
|
||||
|
||||
To fix this, ensure there's only a single non-scalar parameter.
|
||||
If that's not possible, consider converting one or more of the parameters into options, to avoid this limitation.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersNonLastNonScalar(
|
||||
CommandSchema command,
|
||||
CommandParameterSchema invalidParameter)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order:
|
||||
{invalidParameter.Property.Name}
|
||||
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object).
|
||||
Only one parameter in a command may be non-scalar and it must be the last one in order.
|
||||
|
||||
To fix this, ensure that the non-scalar parameter is last in order.
|
||||
If that's not possible, consider converting the parameter into an option, to avoid this limitation.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsNoName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains one or more options without a name:
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))}
|
||||
|
||||
Options must have either a name or a short name or both, because that's what identifies them.
|
||||
|
||||
To fix this, ensure all options have their names or short names set to some values.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsInvalidLengthName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains one or more options whose names are too short:
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(p => $"{p.Property.Name} ('{p.DisplayName}')"))}
|
||||
|
||||
Option names must be at least 2 characters long to avoid confusion with short names.
|
||||
If you intended to set the short name instead, use the corresponding attribute overload.
|
||||
|
||||
To fix this, ensure all option names are at least 2 characters long.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateName(
|
||||
CommandSchema command,
|
||||
string name,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same name ('{name}'):
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))}
|
||||
|
||||
Options must have unique names, because that's what identifies them.
|
||||
Names are not case-sensitive.
|
||||
|
||||
To fix this, ensure that all options have different names.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateShortName(
|
||||
CommandSchema command,
|
||||
char shortName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same short name ('{shortName}'):
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))}
|
||||
|
||||
Options must have unique short names, because that's what identifies them.
|
||||
Short names are case-sensitive (i.e. 'a' and 'A' are different short names).
|
||||
|
||||
To fix this, ensure that all options have different short names.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateEnvironmentVariableName(
|
||||
CommandSchema command,
|
||||
string environmentVariableName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same fallback environment variable name ('{environmentVariableName}'):
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))}
|
||||
|
||||
Options cannot share the same environment variable as a fallback.
|
||||
Environment variable names are not case-sensitive.
|
||||
|
||||
To fix this, ensure that all options have different fallback environment variables.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// End-user-facing exceptions
|
||||
// Avoid internal details and fix recommendations here
|
||||
public partial class CliFxException
|
||||
{
|
||||
internal static CliFxException CannotFindMatchingCommand(CommandLineInput input)
|
||||
{
|
||||
var message = $@"
|
||||
Can't find a command that matches the following arguments:
|
||||
{string.Join(" ", input.UnboundArguments.Select(a => a.Value))}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
|
||||
CommandArgumentSchema argument,
|
||||
IReadOnlyList<string> values)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"Parameter <{argument.DisplayName}>"
|
||||
: $"Option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
{argumentDisplayText} expects a single value, but provided with multiple:
|
||||
{string.Join(", ", values.Select(v => $"'{v}'"))}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertToType(
|
||||
CommandArgumentSchema argument,
|
||||
string? value,
|
||||
Type type,
|
||||
Exception? innerException = null)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"parameter <{argument.DisplayName}>"
|
||||
: $"option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
Can't convert value '{value ?? "<null>"}' to type '{type.FullName}' for {argumentDisplayText}.
|
||||
{innerException?.Message ?? "This type is not supported."}";
|
||||
|
||||
return new CliFxException(message.Trim(), innerException);
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertNonScalar(
|
||||
CommandArgumentSchema argument,
|
||||
IReadOnlyList<string> values,
|
||||
Type type)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"parameter <{argument.DisplayName}>"
|
||||
: $"option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
Can't convert provided values to type '{type.FullName}' for {argumentDisplayText}:
|
||||
{string.Join(", ", values.Select(v => $"'{v}'"))}
|
||||
|
||||
Target type is not assignable from array and doesn't have a public constructor that takes an array.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException ParameterNotSet(CommandParameterSchema parameter)
|
||||
{
|
||||
var message = $@"
|
||||
Missing value for parameter <{parameter.DisplayName}>.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException RequiredOptionsNotSet(IReadOnlyList<CommandOptionSchema> options)
|
||||
{
|
||||
var message = $@"
|
||||
Missing values for one or more required options:
|
||||
{string.Join(Environment.NewLine, options.Select(o => o.DisplayName))}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> inputs)
|
||||
{
|
||||
var message = $@"
|
||||
Unrecognized parameters provided:
|
||||
{string.Join(Environment.NewLine, inputs.Select(i => $"<{i.Value}>"))}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> inputs)
|
||||
{
|
||||
var message = $@"
|
||||
Unrecognized options provided:
|
||||
{string.Join(Environment.NewLine, inputs.Select(i => i.DisplayAlias))}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,6 @@ namespace CliFx.Internal
|
||||
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
||||
builder.Length > 0 ? builder.Append(value) : builder;
|
||||
|
||||
public static StringBuilder AppendBulletList<T>(this StringBuilder builder, IEnumerable<T> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
builder.Append("- ");
|
||||
builder.Append(item);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||
|
||||
public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace CliFx
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IConsole"/> that wraps the default system console.
|
||||
/// </summary>
|
||||
public class SystemConsole : IConsole
|
||||
public partial class SystemConsole : IConsole
|
||||
{
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
@@ -48,9 +48,9 @@ namespace CliFx
|
||||
/// </summary>
|
||||
public SystemConsole()
|
||||
{
|
||||
Input = new StreamReader(Console.OpenStandardInput(), Console.InputEncoding, false);
|
||||
Output = new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) {AutoFlush = true};
|
||||
Error = new StreamWriter(Console.OpenStandardError(), Console.OutputEncoding) {AutoFlush = true};
|
||||
Input = WrapInput(Console.OpenStandardInput());
|
||||
Output = WrapOutput(Console.OpenStandardOutput());
|
||||
Error = WrapOutput(Console.OpenStandardError());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -77,4 +77,17 @@ namespace CliFx
|
||||
return (_cancellationTokenSource = cts).Token;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SystemConsole
|
||||
{
|
||||
private static StreamReader WrapInput(Stream? stream) =>
|
||||
stream != null
|
||||
? new StreamReader(stream, Console.InputEncoding, false)
|
||||
: StreamReader.Null;
|
||||
|
||||
private static StreamWriter WrapOutput(Stream? stream) =>
|
||||
stream != null
|
||||
? new StreamWriter(stream, Console.OutputEncoding) {AutoFlush = true}
|
||||
: StreamWriter.Null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user