Improve exceptions

This commit is contained in:
Alexey Golub
2020-04-20 16:36:45 +03:00
parent 7d3ba612c4
commit 65b66b0d27
15 changed files with 584 additions and 279 deletions

View File

@@ -4,11 +4,16 @@ using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests
{ {
public partial class ApplicationSpecs public partial class ApplicationSpecs
{ {
private readonly ITestOutputHelper _output;
public ApplicationSpecs(ITestOutputHelper output) => _output = output;
[Fact] [Fact]
public void Application_can_be_created_with_a_default_configuration() public void Application_can_be_created_with_a_default_configuration()
{ {
@@ -52,7 +57,8 @@ namespace CliFx.Tests
var commandTypes = Array.Empty<Type>(); var commandTypes = Array.Empty<Type>();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -62,7 +68,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(NonImplementedCommand)}; var commandTypes = new[] {typeof(NonImplementedCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -72,7 +79,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(NonAnnotatedCommand)}; var commandTypes = new[] {typeof(NonAnnotatedCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -82,7 +90,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)}; var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -92,7 +101,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)}; var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -102,7 +112,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)}; var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -112,7 +123,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)}; var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -122,7 +134,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)}; var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -132,7 +145,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(EmptyOptionNameCommand)}; var commandTypes = new[] {typeof(EmptyOptionNameCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -142,7 +156,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)}; var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -152,7 +167,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)}; var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -162,7 +178,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)}; var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -172,7 +189,8 @@ namespace CliFx.Tests
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}; var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes)); var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]

View File

@@ -5,11 +5,16 @@ using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests
{ {
public partial class ArgumentBindingSpecs public partial class ArgumentBindingSpecs
{ {
private readonly ITestOutputHelper _output;
public ArgumentBindingSpecs(ITestOutputHelper output) => _output = output;
[Fact] [Fact]
public void Property_of_type_object_is_bound_directly_from_the_argument_value() public void Property_of_type_object_is_bound_directly_from_the_argument_value()
{ {
@@ -943,7 +948,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -957,7 +963,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -996,7 +1003,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1010,7 +1018,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1024,7 +1033,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1038,7 +1048,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1052,7 +1063,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1067,7 +1079,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -1084,7 +1097,8 @@ namespace CliFx.Tests
.Build(); .Build();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input)); var ex = Assert.Throws<CliFxException>(() => schema.InitializeEntryPoint(input));
_output.WriteLine(ex.Message);
} }
} }
} }

View File

@@ -1,11 +1,16 @@
using CliFx.Exceptions; using CliFx.Exceptions;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests namespace CliFx.Tests
{ {
public partial class DependencyInjectionSpecs public partial class DependencyInjectionSpecs
{ {
private readonly ITestOutputHelper _output;
public DependencyInjectionSpecs(ITestOutputHelper output) => _output = output;
[Fact] [Fact]
public void Default_type_activator_can_initialize_a_command_if_it_has_a_parameterless_constructor() 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(); var activator = new DefaultTypeActivator();
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(typeof(WithDependenciesCommand)));
activator.CreateInstance(typeof(WithDependenciesCommand))); _output.WriteLine(ex.Message);
} }
[Fact] [Fact]
@@ -48,11 +53,11 @@ namespace CliFx.Tests
public void Delegate_type_activator_throws_if_the_underlying_function_returns_null() public void Delegate_type_activator_throws_if_the_underlying_function_returns_null()
{ {
// Arrange // Arrange
var activator = new DelegateTypeActivator(_ => null); var activator = new DelegateTypeActivator(_ => null!);
// Act & assert // Act & assert
Assert.Throws<CliFxException>(() => var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(typeof(WithDependenciesCommand)));
activator.CreateInstance(typeof(WithDependenciesCommand))); _output.WriteLine(ex.Message);
} }
} }
} }

View File

@@ -173,10 +173,10 @@ namespace CliFx
public partial class CliApplicationBuilder 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 // Entry assembly is null in tests
private static Assembly EntryAssembly => LazyEntryAssembly.Value; private static Assembly? EntryAssembly => LazyEntryAssembly.Value;
private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name; private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
@@ -184,14 +184,12 @@ namespace CliFx
{ {
var entryAssemblyLocation = EntryAssembly?.Location; var entryAssemblyLocation = EntryAssembly?.Location;
// If it's a .dll assembly, prepend 'dotnet' and keep the file extension // The assembly can be an executable or a dll, depending on how it was packaged
if (string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase)) var isDll = string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase);
{
return "dotnet " + Path.GetFileName(entryAssemblyLocation);
}
// Otherwise just use assembly file name without extension return isDll
return Path.GetFileNameWithoutExtension(entryAssemblyLocation); ? "dotnet " + Path.GetFileName(entryAssemblyLocation)
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
} }
private static string? GetDefaultVersionText() => private static string? GetDefaultVersionText() =>

View File

@@ -18,11 +18,7 @@ namespace CliFx
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.DefaultActivatorFailed(type, ex);
.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);
} }
} }
} }

View File

@@ -18,10 +18,6 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public object CreateInstance(Type type) => public object CreateInstance(Type type) =>
_func(type) ?? throw new CliFxException(new StringBuilder() _func(type) ?? throw CliFxException.DelegateActivatorReceivedNull(type);
.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());
} }
} }

View File

@@ -1,10 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal;
namespace CliFx.Domain namespace CliFx.Domain
{ {
@@ -71,18 +68,14 @@ namespace CliFx.Domain
IReadOnlyDictionary<string, string> environmentVariables, IReadOnlyDictionary<string, string> environmentVariables,
ITypeActivator activator) ITypeActivator activator)
{ {
var command = TryFindCommand(commandLineInput, out var argumentOffset); var command = TryFindCommand(commandLineInput, out var argumentOffset) ??
if (command == null) throw CliFxException.CannotFindMatchingCommand(commandLineInput);
{
throw new CliFxException(
$"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.UnboundArguments)}].");
}
var parameterValues = argumentOffset == 0 var parameterInputs = argumentOffset == 0
? commandLineInput.UnboundArguments.Select(a => a.Value).ToArray() ? commandLineInput.UnboundArguments.ToArray()
: commandLineInput.UnboundArguments.Skip(argumentOffset).Select(a => a.Value).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( public ICommand InitializeEntryPoint(
@@ -106,28 +99,23 @@ namespace CliFx.Domain
if (duplicateOrderGroup != null) if (duplicateOrderGroup != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandParametersDuplicateOrder(
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same order ({duplicateOrderGroup.Key}):") command,
.AppendBulletList(duplicateOrderGroup.Select(o => o.Property.Name)) duplicateOrderGroup.Key,
.AppendLine() duplicateOrderGroup.ToArray());
.Append("Parameters in a command must all have unique order.")
.ToString());
} }
var duplicateNameGroup = command.Parameters var duplicateNameGroup = command.Parameters
.Where(a => !string.IsNullOrWhiteSpace(a.Name)) .Where(a => !string.IsNullOrWhiteSpace(a.Name))
.GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase) .GroupBy(a => a.Name!, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1); .FirstOrDefault(g => g.Count() > 1);
if (duplicateNameGroup != null) if (duplicateNameGroup != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandParametersDuplicateName(
.AppendLine($"Command {command.Type.FullName} contains two or more parameters that have the same name ({duplicateNameGroup.Key}):") command,
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name)) duplicateNameGroup.Key,
.AppendLine() duplicateNameGroup.ToArray());
.Append("Parameters in a command must all have unique names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
} }
var nonScalarParameters = command.Parameters var nonScalarParameters = command.Parameters
@@ -136,13 +124,9 @@ namespace CliFx.Domain
if (nonScalarParameters.Length > 1) if (nonScalarParameters.Length > 1)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandParametersTooManyNonScalar(
.AppendLine($"Command [{command.Type.FullName}] contains two or more parameters of an enumerable type:") command,
.AppendBulletList(nonScalarParameters.Select(o => o.Property.Name)) nonScalarParameters);
.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());
} }
var nonLastNonScalarParameter = command.Parameters var nonLastNonScalarParameter = command.Parameters
@@ -152,92 +136,74 @@ namespace CliFx.Domain
if (nonLastNonScalarParameter != null) if (nonLastNonScalarParameter != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandParametersNonLastNonScalar(
.AppendLine($"Command {command.Type.FullName} contains a parameter of an enumerable type which doesn't appear last in order:") command,
.AppendLine($"- {nonLastNonScalarParameter.Property.Name}") nonLastNonScalarParameter);
.AppendLine()
.Append("Parameter of an enumerable type must always come last to avoid ambiguity.")
.ToString());
} }
} }
private static void ValidateOptions(CommandSchema command) private static void ValidateOptions(CommandSchema command)
{ {
var emptyNameGroup = command.Options var noNameGroup = command.Options
.Where(o => o.ShortName == null && string.IsNullOrWhiteSpace(o.Name)) .Where(o => o.ShortName == null && string.IsNullOrWhiteSpace(o.Name))
.ToArray(); .ToArray();
if (emptyNameGroup.Any()) if (noNameGroup.Any())
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandOptionsNoName(
.AppendLine($"Command {command.Type.FullName} contains one or more options that have empty names:") command,
.AppendBulletList(emptyNameGroup.Select(o => o.Property.Name)) noNameGroup.ToArray());
.AppendLine()
.Append("Options in a command must all have at least a name or a short name.")
.ToString());
} }
var invalidNameGroup = command.Options var invalidLengthNameGroup = command.Options
.Where(o => !string.IsNullOrWhiteSpace(o.Name)) .Where(o => !string.IsNullOrWhiteSpace(o.Name))
.Where(o => o.Name.Length <= 1) .Where(o => o.Name!.Length <= 1)
.ToArray(); .ToArray();
if (invalidNameGroup.Any()) if (invalidLengthNameGroup.Any())
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandOptionsInvalidLengthName(
.AppendLine($"Command {command.Type.FullName} contains one or more options that have names that are too short:") command,
.AppendBulletList(invalidNameGroup.Select(o => o.Property.Name)) invalidLengthNameGroup);
.AppendLine()
.Append("Options in a command must all have names that are longer than a single character.")
.ToString());
} }
var duplicateNameGroup = command.Options var duplicateNameGroup = command.Options
.Where(o => !string.IsNullOrWhiteSpace(o.Name)) .Where(o => !string.IsNullOrWhiteSpace(o.Name))
.GroupBy(o => o.Name, StringComparer.OrdinalIgnoreCase) .GroupBy(o => o.Name!, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1); .FirstOrDefault(g => g.Count() > 1);
if (duplicateNameGroup != null) if (duplicateNameGroup != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandOptionsDuplicateName(
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same name ({duplicateNameGroup.Key}):") command,
.AppendBulletList(duplicateNameGroup.Select(o => o.Property.Name)) duplicateNameGroup.Key,
.AppendLine() duplicateNameGroup.ToArray());
.Append("Options in a command must all have unique names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
} }
var duplicateShortNameGroup = command.Options var duplicateShortNameGroup = command.Options
.Where(o => o.ShortName != null) .Where(o => o.ShortName != null)
.GroupBy(o => o.ShortName) .GroupBy(o => o.ShortName!.Value)
.FirstOrDefault(g => g.Count() > 1); .FirstOrDefault(g => g.Count() > 1);
if (duplicateShortNameGroup != null) if (duplicateShortNameGroup != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandOptionsDuplicateShortName(
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same short name ({duplicateShortNameGroup.Key}):") command,
.AppendBulletList(duplicateShortNameGroup.Select(o => o.Property.Name)) duplicateShortNameGroup.Key,
.AppendLine() duplicateShortNameGroup.ToArray());
.Append("Options in a command must all have unique short names.").Append(" ")
.Append("Comparison is case-sensitive.")
.ToString());
} }
var duplicateEnvironmentVariableNameGroup = command.Options var duplicateEnvironmentVariableNameGroup = command.Options
.Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName)) .Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName))
.GroupBy(o => o.EnvironmentVariableName, StringComparer.OrdinalIgnoreCase) .GroupBy(o => o.EnvironmentVariableName!, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(g => g.Count() > 1); .FirstOrDefault(g => g.Count() > 1);
if (duplicateEnvironmentVariableNameGroup != null) if (duplicateEnvironmentVariableNameGroup != null)
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.CommandOptionsDuplicateEnvironmentVariableName(
.AppendLine($"Command {command.Type.FullName} contains two or more options that have the same environment variable name ({duplicateEnvironmentVariableNameGroup.Key}):") command,
.AppendBulletList(duplicateEnvironmentVariableNameGroup.Select(o => o.Property.Name)) duplicateEnvironmentVariableNameGroup.Key,
.AppendLine() duplicateEnvironmentVariableNameGroup.ToArray());
.Append("Options in a command must all have unique environment variable names.").Append(" ")
.Append("Comparison is NOT case-sensitive.")
.ToString());
} }
} }
@@ -245,7 +211,7 @@ namespace CliFx.Domain
{ {
if (!commands.Any()) if (!commands.Any())
{ {
throw new CliFxException("There are no commands configured for this application."); throw CliFxException.CommandsNotRegistered();
} }
var duplicateNameGroup = commands var duplicateNameGroup = commands
@@ -254,13 +220,12 @@ namespace CliFx.Domain
if (duplicateNameGroup != null) if (duplicateNameGroup != null)
{ {
throw new CliFxException(new StringBuilder() if (!string.IsNullOrWhiteSpace(duplicateNameGroup.Key))
.AppendLine($"Application contains two or more commands that have the same name ({duplicateNameGroup.Key}):") throw CliFxException.CommandsDuplicateName(
.AppendBulletList(duplicateNameGroup.Select(o => o.Type.FullName)) duplicateNameGroup.Key,
.AppendLine() duplicateNameGroup.ToArray());
.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.") throw CliFxException.CommandsTooManyDefaults(duplicateNameGroup.ToArray());
.ToString());
} }
} }
@@ -270,17 +235,8 @@ namespace CliFx.Domain
foreach (var commandType in commandTypes) foreach (var commandType in commandTypes)
{ {
var command = CommandSchema.TryResolve(commandType); var command = CommandSchema.TryResolve(commandType) ??
if (command == null) throw CliFxException.InvalidCommandType(commandType);
{
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());
}
ValidateParameters(command); ValidateParameters(command);
ValidateOptions(command); ValidateOptions(command);

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
@@ -15,7 +14,9 @@ namespace CliFx.Domain
public string? Description { get; } 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) protected CommandArgumentSchema(PropertyInfo property, string? description)
{ {
@@ -23,28 +24,85 @@ namespace CliFx.Domain
Description = description; Description = description;
} }
private Type? GetEnumerableArgumentUnderlyingType() => private Type? TryGetEnumerableArgumentUnderlyingType() =>
Property.PropertyType != typeof(string) Property.PropertyType != typeof(string)
? Property.PropertyType.GetEnumerableUnderlyingType() ? Property.PropertyType.GetEnumerableUnderlyingType()
: null; : 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 targetType = Property.PropertyType;
var enumerableUnderlyingType = GetEnumerableArgumentUnderlyingType(); var enumerableUnderlyingType = TryGetEnumerableArgumentUnderlyingType();
// Scalar // Scalar
if (enumerableUnderlyingType == null) if (enumerableUnderlyingType == null)
{ {
if (values.Count > 1) return values.Count <= 1
{ ? ConvertScalar(values.SingleOrDefault(), targetType)
throw new CliFxException(new StringBuilder() : throw CliFxException.CannotConvertMultipleValuesToNonScalar(this, values);
.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);
} }
// Non-scalar // Non-scalar
else else
@@ -64,8 +122,8 @@ namespace CliFx.Domain
{ {
private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture; private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture;
private static readonly IReadOnlyDictionary<Type, Func<string, object>> PrimitiveConverters = private static readonly IReadOnlyDictionary<Type, Func<string?, object?>> PrimitiveConverters =
new Dictionary<Type, Func<string?, object>> new Dictionary<Type, Func<string?, object?>>
{ {
[typeof(object)] = v => v, [typeof(object)] = v => v,
[typeof(string)] = v => v, [typeof(string)] = v => v,
@@ -99,78 +157,5 @@ namespace CliFx.Domain
type.GetMethod("Parse", type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static, BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string), typeof(IFormatProvider)}, null); 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());
}
} }
} }

View File

@@ -8,6 +8,11 @@ namespace CliFx.Domain
{ {
public string Alias { get; } public string Alias { get; }
public string DisplayAlias =>
Alias.Length > 1
? $"--{Alias}"
: $"-{Alias}";
public IReadOnlyList<string> Values { get; } public IReadOnlyList<string> Values { get; }
public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias); public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias);
@@ -24,8 +29,7 @@ namespace CliFx.Domain
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
buffer.Append(Alias.Length > 1 ? "--" : "-"); buffer.Append(DisplayAlias);
buffer.Append(Alias);
foreach (var value in Values) foreach (var value in Values)
{ {

View File

@@ -3,7 +3,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Internal;
namespace CliFx.Domain namespace CliFx.Domain
{ {
@@ -13,9 +12,9 @@ namespace CliFx.Domain
public char? ShortName { get; } public char? ShortName { get; }
public string DisplayName => !string.IsNullOrWhiteSpace(Name) public override string DisplayName => !string.IsNullOrWhiteSpace(Name)
? Name ? $"--{Name}"
: ShortName?.AsString()!; : $"-{ShortName}";
public string? EnvironmentVariableName { get; } public string? EnvironmentVariableName { get; }

View File

@@ -10,9 +10,10 @@ namespace CliFx.Domain
public string? Name { get; } public string? Name { get; }
public string DisplayName => !string.IsNullOrWhiteSpace(Name) public override string DisplayName =>
? Name !string.IsNullOrWhiteSpace(Name)
: Property.Name.ToLowerInvariant(); ? Name
: Property.Name.ToLowerInvariant();
public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description) public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description)
: base(property, description) : base(property, description)

View File

@@ -40,7 +40,7 @@ namespace CliFx.Domain
public bool MatchesName(string? name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase); 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 // All inputs must be bound
var remainingParameterInputs = parameterInputs.ToList(); var remainingParameterInputs = parameterInputs.ToList();
@@ -57,9 +57,9 @@ namespace CliFx.Domain
var scalarParameterInput = i < parameterInputs.Count var scalarParameterInput = i < parameterInputs.Count
? parameterInputs[i] ? 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); remainingParameterInputs.Remove(scalarParameterInput);
} }
@@ -70,18 +70,16 @@ namespace CliFx.Domain
if (nonScalarParameter != null) if (nonScalarParameter != null)
{ {
var nonScalarParameterInputs = parameterInputs.Skip(scalarParameters.Length).ToArray(); var nonScalarParameterValues = parameterInputs.Skip(scalarParameters.Length).Select(i => i.Value).ToArray();
nonScalarParameter.Inject(command, nonScalarParameterInputs);
nonScalarParameter.Inject(command, nonScalarParameterValues);
remainingParameterInputs.Clear(); remainingParameterInputs.Clear();
} }
// Ensure all inputs were bound // Ensure all inputs were bound
if (remainingParameterInputs.Any()) if (remainingParameterInputs.Any())
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.UnrecognizedParametersProvided(remainingParameterInputs);
.AppendLine("Unrecognized parameters provided:")
.AppendBulletList(remainingParameterInputs)
.ToString());
} }
} }
@@ -136,24 +134,18 @@ namespace CliFx.Domain
// Ensure all required options were set // Ensure all required options were set
if (unsetRequiredOptions.Any()) if (unsetRequiredOptions.Any())
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions);
.AppendLine("Missing values for some of the required options:")
.AppendBulletList(unsetRequiredOptions.Select(o => o.DisplayName))
.ToString());
} }
// Ensure all inputs were bound // Ensure all inputs were bound
if (remainingOptionInputs.Any()) if (remainingOptionInputs.Any())
{ {
throw new CliFxException(new StringBuilder() throw CliFxException.UnrecognizedOptionsProvided(remainingOptionInputs);
.AppendLine("Unrecognized options provided:")
.AppendBulletList(remainingOptionInputs.Select(o => o.Alias).Distinct())
.ToString());
} }
} }
public ICommand CreateInstance( public ICommand CreateInstance(
IReadOnlyList<string> parameterInputs, IReadOnlyList<CommandUnboundArgumentInput> parameterInputs,
IReadOnlyList<CommandOptionInput> optionInputs, IReadOnlyList<CommandOptionInput> optionInputs,
IReadOnlyDictionary<string, string> environmentVariables, IReadOnlyDictionary<string, string> environmentVariables,
ITypeActivator activator) ITypeActivator activator)

View File

@@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Attributes;
using CliFx.Domain;
namespace CliFx.Exceptions namespace CliFx.Exceptions
{ {
/// <summary> /// <summary>
/// Domain exception thrown within CliFx. /// Domain exception thrown within CliFx.
/// </summary> /// </summary>
public class CliFxException : Exception public partial class CliFxException : Exception
{ {
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CliFxException"/>. /// 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());
}
}
} }

View File

@@ -15,18 +15,6 @@ namespace CliFx.Internal
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) => public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
builder.Length > 0 ? builder.Append(value) : builder; 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 bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type); public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);

View File

@@ -7,7 +7,7 @@ namespace CliFx
/// <summary> /// <summary>
/// Implementation of <see cref="IConsole"/> that wraps the default system console. /// Implementation of <see cref="IConsole"/> that wraps the default system console.
/// </summary> /// </summary>
public class SystemConsole : IConsole public partial class SystemConsole : IConsole
{ {
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
@@ -48,9 +48,9 @@ namespace CliFx
/// </summary> /// </summary>
public SystemConsole() public SystemConsole()
{ {
Input = new StreamReader(Console.OpenStandardInput(), Console.InputEncoding, false); Input = WrapInput(Console.OpenStandardInput());
Output = new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) {AutoFlush = true}; Output = WrapOutput(Console.OpenStandardOutput());
Error = new StreamWriter(Console.OpenStandardError(), Console.OutputEncoding) {AutoFlush = true}; Error = WrapOutput(Console.OpenStandardError());
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -77,4 +77,17 @@ namespace CliFx
return (_cancellationTokenSource = cts).Token; 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;
}
} }