diff --git a/CliFx.Tests/ApplicationSpecs.cs b/CliFx.Tests/ApplicationSpecs.cs index 6cfcdc1..cd94fc7 100644 --- a/CliFx.Tests/ApplicationSpecs.cs +++ b/CliFx.Tests/ApplicationSpecs.cs @@ -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(); // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -62,7 +68,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(NonImplementedCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -72,7 +79,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(NonAnnotatedCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => 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(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -92,7 +101,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -102,7 +112,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(DuplicateParameterNameCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -112,7 +123,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -122,7 +134,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -132,7 +145,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(EmptyOptionNameCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -142,7 +156,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -152,7 +167,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -162,7 +178,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] @@ -172,7 +189,8 @@ namespace CliFx.Tests var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}; // Act & assert - Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + _output.WriteLine(ex.Message); } [Fact] diff --git a/CliFx.Tests/ArgumentBindingSpecs.cs b/CliFx.Tests/ArgumentBindingSpecs.cs index 1d6873f..14e9f31 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.cs @@ -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(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -957,7 +963,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -996,7 +1003,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1010,7 +1018,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1024,7 +1033,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1038,7 +1048,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1052,7 +1063,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1067,7 +1079,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } [Fact] @@ -1084,7 +1097,8 @@ namespace CliFx.Tests .Build(); // Act & assert - Assert.Throws(() => schema.InitializeEntryPoint(input)); + var ex = Assert.Throws(() => schema.InitializeEntryPoint(input)); + _output.WriteLine(ex.Message); } } } \ No newline at end of file diff --git a/CliFx.Tests/DependencyInjectionSpecs.cs b/CliFx.Tests/DependencyInjectionSpecs.cs index 3100a20..e23a56e 100644 --- a/CliFx.Tests/DependencyInjectionSpecs.cs +++ b/CliFx.Tests/DependencyInjectionSpecs.cs @@ -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(() => - activator.CreateInstance(typeof(WithDependenciesCommand))); + var ex = Assert.Throws(() => 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(() => - activator.CreateInstance(typeof(WithDependenciesCommand))); + var ex = Assert.Throws(() => activator.CreateInstance(typeof(WithDependenciesCommand))); + _output.WriteLine(ex.Message); } } } \ No newline at end of file diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index 48b708d..e6f2dd1 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -173,10 +173,10 @@ namespace CliFx public partial class CliApplicationBuilder { - private static readonly Lazy LazyEntryAssembly = new Lazy(Assembly.GetEntryAssembly); + private static readonly Lazy LazyEntryAssembly = new Lazy(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() => diff --git a/CliFx/DefaultTypeActivator.cs b/CliFx/DefaultTypeActivator.cs index 0178a04..3abd433 100644 --- a/CliFx/DefaultTypeActivator.cs +++ b/CliFx/DefaultTypeActivator.cs @@ -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); } } } diff --git a/CliFx/DelegateTypeActivator.cs b/CliFx/DelegateTypeActivator.cs index 55cac46..72110cc 100644 --- a/CliFx/DelegateTypeActivator.cs +++ b/CliFx/DelegateTypeActivator.cs @@ -18,10 +18,6 @@ namespace CliFx /// public object CreateInstance(Type type) => - _func(type) ?? throw new CliFxException(new StringBuilder() - .Append($"Failed to create an instance of type {type.FullName}, received 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); } } \ No newline at end of file diff --git a/CliFx/Domain/ApplicationSchema.cs b/CliFx/Domain/ApplicationSchema.cs index 94100e8..f813a1e 100644 --- a/CliFx/Domain/ApplicationSchema.cs +++ b/CliFx/Domain/ApplicationSchema.cs @@ -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 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); diff --git a/CliFx/Domain/CommandArgumentSchema.cs b/CliFx/Domain/CommandArgumentSchema.cs index 02b1a9d..f0852f9 100644 --- a/CliFx/Domain/CommandArgumentSchema.cs +++ b/CliFx/Domain/CommandArgumentSchema.cs @@ -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 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 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 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> PrimitiveConverters = - new Dictionary> + private static readonly IReadOnlyDictionary> PrimitiveConverters = + new Dictionary> { [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 ?? ""}' to type {targetType.FullName}.") - .Append(ex.Message) - .ToString(), ex); - } - - throw new CliFxException(new StringBuilder() - .AppendLine($"Can't convert value '{value ?? ""}' to type {targetType.FullName}.") - .Append("Target type is not supported by CliFx.") - .ToString()); - } - - private static object ConvertNonScalar(IReadOnlyList 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()); - } } } \ No newline at end of file diff --git a/CliFx/Domain/CommandOptionInput.cs b/CliFx/Domain/CommandOptionInput.cs index adb2780..dfc3198 100644 --- a/CliFx/Domain/CommandOptionInput.cs +++ b/CliFx/Domain/CommandOptionInput.cs @@ -8,6 +8,11 @@ namespace CliFx.Domain { public string Alias { get; } + public string DisplayAlias => + Alias.Length > 1 + ? $"--{Alias}" + : $"-{Alias}"; + public IReadOnlyList 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) { diff --git a/CliFx/Domain/CommandOptionSchema.cs b/CliFx/Domain/CommandOptionSchema.cs index 3acc2fa..bfd4553 100644 --- a/CliFx/Domain/CommandOptionSchema.cs +++ b/CliFx/Domain/CommandOptionSchema.cs @@ -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; } diff --git a/CliFx/Domain/CommandParameterSchema.cs b/CliFx/Domain/CommandParameterSchema.cs index b1e8da9..00b309d 100644 --- a/CliFx/Domain/CommandParameterSchema.cs +++ b/CliFx/Domain/CommandParameterSchema.cs @@ -10,9 +10,10 @@ namespace CliFx.Domain public string? Name { get; } - public string DisplayName => !string.IsNullOrWhiteSpace(Name) - ? Name - : Property.Name.ToLowerInvariant(); + public override string DisplayName => + !string.IsNullOrWhiteSpace(Name) + ? Name + : Property.Name.ToLowerInvariant(); public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description) : base(property, description) diff --git a/CliFx/Domain/CommandSchema.cs b/CliFx/Domain/CommandSchema.cs index 682db9c..f797cb9 100644 --- a/CliFx/Domain/CommandSchema.cs +++ b/CliFx/Domain/CommandSchema.cs @@ -40,7 +40,7 @@ namespace CliFx.Domain public bool MatchesName(string? name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase); - private void InjectParameters(ICommand command, IReadOnlyList parameterInputs) + private void InjectParameters(ICommand command, IReadOnlyList 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 parameterInputs, + IReadOnlyList parameterInputs, IReadOnlyList optionInputs, IReadOnlyDictionary environmentVariables, ITypeActivator activator) diff --git a/CliFx/Exceptions/CliFxException.cs b/CliFx/Exceptions/CliFxException.cs index 7e04e9d..47eaa9b 100644 --- a/CliFx/Exceptions/CliFxException.cs +++ b/CliFx/Exceptions/CliFxException.cs @@ -1,11 +1,15 @@ using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Attributes; +using CliFx.Domain; namespace CliFx.Exceptions { /// /// Domain exception thrown within CliFx. /// - public class CliFxException : Exception + public partial class CliFxException : Exception { /// /// Initializes an instance of . @@ -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 instead. + +To fix this, ensure that the provided type activator was configured correctly, as it's not expected to return . +If you are using a dependency container, ensure this type is registered, because it may return 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 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 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 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 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 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 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 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 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 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 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 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 ?? ""}' 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 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 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 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 inputs) + { + var message = $@" +Unrecognized options provided: +{string.Join(Environment.NewLine, inputs.Select(i => i.DisplayAlias))}"; + + return new CliFxException(message.Trim()); + } + } } \ No newline at end of file diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs index 160da8a..8008611 100644 --- a/CliFx/Internal/Extensions.cs +++ b/CliFx/Internal/Extensions.cs @@ -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(this StringBuilder builder, IEnumerable 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); diff --git a/CliFx/SystemConsole.cs b/CliFx/SystemConsole.cs index 566e843..925cdcb 100644 --- a/CliFx/SystemConsole.cs +++ b/CliFx/SystemConsole.cs @@ -7,7 +7,7 @@ namespace CliFx /// /// Implementation of that wraps the default system console. /// - public class SystemConsole : IConsole + public partial class SystemConsole : IConsole { private CancellationTokenSource? _cancellationTokenSource; @@ -48,9 +48,9 @@ namespace CliFx /// 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()); } /// @@ -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; + } } \ No newline at end of file