This commit is contained in:
Alexey Golub
2020-05-16 21:54:16 +03:00
parent f5e37b96fc
commit 4732166f5f
20 changed files with 641 additions and 707 deletions

View File

@@ -51,6 +51,8 @@ namespace CliFx.Tests
console.ResetColor(); console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta; console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta; console.BackgroundColor = ConsoleColor.DarkMagenta;
console.CursorLeft = 42;
console.CursorTop = 24;
// Assert // Assert
stdInData.Should().Be("input"); stdInData.Should().Be("input");

View File

@@ -1,11 +1,7 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions;
namespace CliFx.Tests namespace CliFx.Tests
{ {
@@ -89,16 +85,16 @@ namespace CliFx.Tests
[Command("cmd-with-enum-args")] [Command("cmd-with-enum-args")]
private class EnumArgumentsCommand : ICommand private class EnumArgumentsCommand : ICommand
{ {
public enum TestEnum { Value1, Value2, Value3 }; public enum CustomEnum { Value1, Value2, Value3 };
[CommandParameter(0, Name = "value", Description = "Enum parameter.")] [CommandParameter(0, Name = "value", Description = "Enum parameter.")]
public TestEnum ParamA { get; set; } public CustomEnum ParamA { get; set; }
[CommandOption("value", Description = "Enum option.", IsRequired = true)] [CommandOption("value", Description = "Enum option.", IsRequired = true)]
public TestEnum OptionA { get; set; } = TestEnum.Value1; public CustomEnum OptionA { get; set; } = CustomEnum.Value1;
[CommandOption("nullable-value", Description = "Nullable enum option.")] [CommandOption("nullable-value", Description = "Nullable enum option.")]
public TestEnum? OptionB { get; set; } public CustomEnum? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }
@@ -116,8 +112,10 @@ namespace CliFx.Tests
} }
[Command("cmd-with-defaults")] [Command("cmd-with-defaults")]
private class DefaultArgumentsCommand : ICommand private class ArgumentsWithDefaultValuesCommand : ICommand
{ {
public enum CustomEnum { Value1, Value2, Value3 };
[CommandOption(nameof(Object))] [CommandOption(nameof(Object))]
public object? Object { get; set; } = 42; public object? Object { get; set; } = 42;
@@ -127,98 +125,29 @@ namespace CliFx.Tests
[CommandOption(nameof(EmptyString))] [CommandOption(nameof(EmptyString))]
public string EmptyString { get; set; } = ""; public string EmptyString { get; set; } = "";
[CommandOption(nameof(WhiteSpaceString))]
public string WhiteSpaceString { get; set; } = " ";
[CommandOption(nameof(Bool))] [CommandOption(nameof(Bool))]
public bool Bool { get; set; } = true; public bool Bool { get; set; } = true;
[CommandOption(nameof(Char))] [CommandOption(nameof(Char))]
public char Char { get; set; } = 't'; public char Char { get; set; } = 't';
[CommandOption(nameof(Sbyte))]
public sbyte Sbyte { get; set; } = -0b11;
[CommandOption(nameof(Byte))]
public byte Byte { get; set; } = 0b11;
[CommandOption(nameof(Short))]
public short Short { get; set; } = -1234;
[CommandOption(nameof(Ushort))]
public short Ushort { get; set; } = 1234;
[CommandOption(nameof(Int))] [CommandOption(nameof(Int))]
public int Int { get; set; } = 1337; public int Int { get; set; } = 1337;
[CommandOption(nameof(Uint))]
public uint Uint { get; set; } = 2345;
[CommandOption(nameof(Long))]
public long Long { get; set; } = -1234567;
[CommandOption(nameof(Ulong))]
public ulong Ulong { get; set; } = 12345678;
[CommandOption(nameof(Float))]
public float Float { get; set; } = 123.4567F;
[CommandOption(nameof(Double))]
public double Double { get; set; } = 420.1337;
[CommandOption(nameof(Decimal))]
public decimal Decimal { get; set; } = 1337.420M;
[CommandOption(nameof(DateTime))]
public DateTime DateTime { get; set; } = new DateTime(2020, 4, 20);
[CommandOption(nameof(DateTimeOffset))]
public DateTimeOffset DateTimeOffset { get; set; } =
new DateTimeOffset(2008, 5, 1, 0, 0, 0, new TimeSpan(0, 1, 0, 0, 0));
[CommandOption(nameof(TimeSpan))] [CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123); public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
public enum TestEnum { Value1, Value2, Value3 }; [CommandOption(nameof(Enum))]
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
[CommandOption(nameof(CustomEnum))]
public TestEnum CustomEnum { get; set; } = TestEnum.Value2;
[CommandOption(nameof(IntNullable))] [CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; } = 1337; public int? IntNullable { get; set; } = 1337;
[CommandOption(nameof(CustomEnumNullable))]
public TestEnum? CustomEnumNullable { get; set; } = TestEnum.Value2;
[CommandOption(nameof(TimeSpanNullable))]
public TimeSpan? TimeSpanNullable { get; set; } = TimeSpan.FromMinutes(234);
[CommandOption(nameof(ObjectArray))]
public object[]? ObjectArray { get; set; } = new object[] { "123", 4, 3.14 };
[CommandOption(nameof(StringArray))] [CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; } = new[] { "foo", "bar", "baz" }; public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
[CommandOption(nameof(IntArray))] [CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; } = new[] { 1, 2, 3 }; public int[]? IntArray { get; set; } = { 1, 2, 3 };
[CommandOption(nameof(CustomEnumArray))]
public TestEnum[]? CustomEnumArray { get; set; } = new[] { TestEnum.Value1, TestEnum.Value3 };
[CommandOption(nameof(IntNullableArray))]
public int?[]? IntNullableArray { get; set; } = new int?[] { 2, 3, 4, null, 5 };
[CommandOption(nameof(EnumerableNullable))]
public IEnumerable? EnumerableNullable { get; set; } = Enumerable.Repeat("foo", 3);
[CommandOption(nameof(StringEnumerable))]
public IEnumerable<string>? StringEnumerable { get; set; } = Enumerable.Repeat("bar", 3);
[CommandOption(nameof(StringReadOnlyList))]
public IReadOnlyList<string>? StringReadOnlyList { get; set; } = new[] { "foo", "bar", "baz" };
[CommandOption(nameof(StringList))]
public List<string>? StringList { get; set; } = new List<string>() { "foo", "bar", "baz" };
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;
} }

View File

@@ -269,7 +269,7 @@ namespace CliFx.Tests
_output.WriteLine(stdOutData); _output.WriteLine(stdOutData);
} }
[Fact] [Fact]
public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined() public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined()
{ {
@@ -304,51 +304,30 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut); var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultArgumentsCommand)) .AddCommand(typeof(ArgumentsWithDefaultValuesCommand))
.UseConsole(console) .UseConsole(console)
.Build(); .Build();
// Act // Act
await application.RunAsync(new[] { "cmd-with-defaults", "--help" }); await application.RunAsync(new[] { "cmd-with-defaults", "--help" });
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert // Assert
stdOutData.Should().ContainAll( stdOutData.Should().ContainAll(
"Usage", "Usage",
"cmd-with-defaults", "[options]", "cmd-with-defaults", "[options]",
"Options", "Options",
"--Object", "(Default: 42)", "--Object", "Default: \"42\"",
"--String", "(Default: foo)", "--String", "Default: \"foo\"",
"--EmptyString", "(Default: \"\"", "--EmptyString", "Default: \"\"",
"--WhiteSpaceString", "(Default: \" \"", "--Bool", "Default: \"True\"",
"--Bool", "(Default: True)", "--Char", "Default: \"t\"",
"--Char", "(Default: t)", "--Int", "Default: \"1337\"",
"--Sbyte", "(Default: -3)", "--TimeSpan", "Default: \"02:03:00\"",
"--Byte", "(Default: 3)", "--Enum", "Default: \"Value2\"",
"--Short", "(Default: -1234)", "--IntNullable", "Default: \"1337\"",
"--Ushort", "(Default: 1234)", "--StringArray", "Default: \"foo\" \"bar\" \"baz\"",
"--Int", "(Default: 1337)", "--IntArray", "Default: \"1\" \"2\" \"3\""
"--Uint", "(Default: 2345)",
"--Long", "(Default: -1234567)",
"--Ulong", "(Default: 12345678)",
"--Float", "(Default: 123.4567)",
"--Double", "(Default: 420.1337)",
"--Decimal", "(Default: 1337.420)",
"--DateTime", $"(Default: {new DateTime(2020, 4, 20)}",
"--DateTimeOffset", $"(Default: {new DateTimeOffset(2008, 5, 1, 0, 0, 0, new TimeSpan(0, 1, 0, 0, 0))}",
"--TimeSpan", "(Default: 02:03:00)",
"--IntNullable", "(Default: 1337)",
"--CustomEnumNullable", "(Default: Value2)",
"--TimeSpanNullable", "(Default: 03:54:00)",
"--ObjectArray", "(Default: 123 4 3.14)",
"--StringArray", "(Default: foo bar baz)",
"--IntArray", "(Default: 1 2 3)",
"--CustomEnumArray", "(Default: Value1 Value3)",
"--IntNullableArray", "(Default: 2 3 4 5)",
"--EnumerableNullable", "(Default: foo foo foo)",
"--StringEnumerable", "(Default: bar bar bar)",
"--StringReadOnlyList", "(Default: foo bar baz)",
"--StringList", "(Default: foo bar baz)"
); );
_output.WriteLine(stdOutData); _output.WriteLine(stdOutData);

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Domain; using CliFx.Domain;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal;
namespace CliFx namespace CliFx
{ {
@@ -33,7 +34,7 @@ namespace CliFx
_console = console; _console = console;
_typeActivator = typeActivator; _typeActivator = typeActivator;
_helpTextWriter = new HelpTextWriter(metadata, console); _helpTextWriter = new HelpTextWriter(metadata, console, typeActivator);
} }
private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput) private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
@@ -42,8 +43,10 @@ namespace CliFx
if (!isDebugMode) if (!isDebugMode)
return null; return null;
var processId = ProcessEx.GetCurrentProcessId();
_console.WithForegroundColor(ConsoleColor.Green, () => _console.WithForegroundColor(ConsoleColor.Green, () =>
_console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue.")); _console.Output.WriteLine($"Attach debugger to PID {processId} to continue."));
while (!Debugger.IsAttached) while (!Debugger.IsAttached)
await Task.Delay(100); await Task.Delay(100);
@@ -124,7 +127,7 @@ namespace CliFx
// Get the command schema that matches the input or use a dummy default command as a fallback // Get the command schema that matches the input or use a dummy default command as a fallback
var commandSchema = var commandSchema =
applicationSchema.TryFindCommand(commandLineInput) ?? applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand; CommandSchema.StubDefaultCommand.Schema;
_helpTextWriter.Write(applicationSchema, commandSchema); _helpTextWriter.Write(applicationSchema, commandSchema);
@@ -143,30 +146,26 @@ namespace CliFx
return 0; return 0;
} }
/// <summary> private int HandleCliFxException(IReadOnlyList<string> commandLineArguments, CliFxException ex)
/// Handle <see cref="CommandException"/>s differently from the rest because we want to
/// display it different based on whether we are showing the help text or not.
/// </summary>
private int HandleCliFxException(IReadOnlyList<string> commandLineArguments, CliFxException cfe)
{ {
var showHelp = cfe.ShowHelp; var showHelp = ex.ShowHelp;
var errorMessage = cfe.HasMessage var errorMessage = ex.HasMessage
? cfe.Message ? ex.Message
: cfe.ToString(); : ex.ToString();
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage)); _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
if (showHelp) if (showHelp)
{ {
var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes); var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
var commandLineInput = CommandLineInput.Parse(commandLineArguments); var commandLineInput = CommandLineInput.Parse(commandLineArguments);
var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ?? var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand; CommandSchema.StubDefaultCommand.Schema;
_helpTextWriter.Write(applicationSchema, commandSchema); _helpTextWriter.Write(applicationSchema, commandSchema);
} }
return cfe.ExitCode; return ex.ExitCode;
} }
/// <summary> /// <summary>
@@ -188,16 +187,15 @@ namespace CliFx
HandleHelpOption(applicationSchema, commandLineInput) ?? HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables); await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
} }
catch (CliFxException cfe) catch (CliFxException ex)
{ {
// We want to catch exceptions in order to print errors and return correct exit codes. // Some exceptions may specify exit code or request help
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions. return HandleCliFxException(commandLineArguments, ex);
var exitCode = HandleCliFxException(commandLineArguments, cfe);
return exitCode;
} }
catch (Exception ex) catch (Exception ex)
{ {
// For all other errors, we just write the entire thing to stderr. // We want to catch all exceptions in order to print errors and return correct exit codes.
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.ToString())); _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.ToString()));
return ex.HResult; return ex.HResult;
} }

View File

@@ -18,6 +18,6 @@ namespace CliFx
/// <inheritdoc /> /// <inheritdoc />
public object CreateInstance(Type type) => public object CreateInstance(Type type) =>
_func(type) ?? throw CliFxException.DelegateActivatorReceivedNull(type); _func(type) ?? throw CliFxException.DelegateActivatorReturnedNull(type);
} }
} }

View File

@@ -15,8 +15,6 @@ namespace CliFx.Domain
public string? Description { get; } public string? Description { get; }
public abstract string DisplayName { get; }
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null; public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
protected CommandArgumentSchema(PropertyInfo property, string? description) protected CommandArgumentSchema(PropertyInfo property, string? description)
@@ -51,17 +49,17 @@ namespace CliFx.Domain
: null; : null;
// String-constructable // String-constructable
var stringConstructor = GetStringConstructor(targetType); var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
if (stringConstructor != null) if (stringConstructor != null)
return stringConstructor.Invoke(new object[] {value!}); return stringConstructor.Invoke(new object[] {value!});
// String-parseable (with format provider) // String-parseable (with format provider)
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType); var parseMethodWithFormatProvider = targetType.GetStaticParseMethod(true);
if (parseMethodWithFormatProvider != null) if (parseMethodWithFormatProvider != null)
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, ConversionFormatProvider}); return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, FormatProvider});
// String-parseable (without format provider) // String-parseable (without format provider)
var parseMethod = GetStaticParseMethod(targetType); var parseMethod = targetType.GetStaticParseMethod();
if (parseMethod != null) if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value!}); return parseMethod.Invoke(null, new object[] {value!});
} }
@@ -117,11 +115,62 @@ namespace CliFx.Domain
public void Inject(ICommand command, params string[] values) => public void Inject(ICommand command, params string[] values) =>
Inject(command, (IReadOnlyList<string>) values); Inject(command, (IReadOnlyList<string>) values);
public IReadOnlyList<string> GetValidValues()
{
var result = new List<string>();
// Some arguments may have this as null due to a hack that enables built-in options
// TODO fix this
if (Property == null)
return result;
var underlyingType =
Property.PropertyType.GetNullableUnderlyingType() ?? Property.PropertyType;
// Enum
if (underlyingType.IsEnum)
result.AddRange(Enum.GetNames(underlyingType));
return result;
}
public string? TryGetDefaultValue(ICommand instance)
{
// Some arguments may have this as null due to a hack that enables built-in options
// TODO fix this
if (Property == null)
return null;
var rawDefaultValue = Property.GetValue(instance);
if (!(rawDefaultValue is string) && rawDefaultValue is IEnumerable rawDefaultValues)
{
var elementType = rawDefaultValues.GetType().GetEnumerableUnderlyingType() ?? typeof(object);
return elementType.IsToStringOverriden()
? rawDefaultValues
.Cast<object?>()
.Where(o => o != null)
.Select(o => o!.ToFormattableString(FormatProvider).Quote())
.JoinToString(" ")
: null;
}
if (rawDefaultValue != null && !Equals(rawDefaultValue, rawDefaultValue.GetType().GetDefaultValue()))
{
return rawDefaultValue.GetType().IsToStringOverriden()
? rawDefaultValue.ToFormattableString(FormatProvider).Quote()
: null;
}
return null;
}
} }
internal partial class CommandArgumentSchema internal partial class CommandArgumentSchema
{ {
private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture; private static readonly IFormatProvider FormatProvider = 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?>>
@@ -130,112 +179,20 @@ namespace CliFx.Domain
[typeof(string)] = v => v, [typeof(string)] = v => v,
[typeof(bool)] = v => string.IsNullOrWhiteSpace(v) || bool.Parse(v), [typeof(bool)] = v => string.IsNullOrWhiteSpace(v) || bool.Parse(v),
[typeof(char)] = v => v.Single(), [typeof(char)] = v => v.Single(),
[typeof(sbyte)] = v => sbyte.Parse(v, ConversionFormatProvider), [typeof(sbyte)] = v => sbyte.Parse(v, FormatProvider),
[typeof(byte)] = v => byte.Parse(v, ConversionFormatProvider), [typeof(byte)] = v => byte.Parse(v, FormatProvider),
[typeof(short)] = v => short.Parse(v, ConversionFormatProvider), [typeof(short)] = v => short.Parse(v, FormatProvider),
[typeof(ushort)] = v => ushort.Parse(v, ConversionFormatProvider), [typeof(ushort)] = v => ushort.Parse(v, FormatProvider),
[typeof(int)] = v => int.Parse(v, ConversionFormatProvider), [typeof(int)] = v => int.Parse(v, FormatProvider),
[typeof(uint)] = v => uint.Parse(v, ConversionFormatProvider), [typeof(uint)] = v => uint.Parse(v, FormatProvider),
[typeof(long)] = v => long.Parse(v, ConversionFormatProvider), [typeof(long)] = v => long.Parse(v, FormatProvider),
[typeof(ulong)] = v => ulong.Parse(v, ConversionFormatProvider), [typeof(ulong)] = v => ulong.Parse(v, FormatProvider),
[typeof(float)] = v => float.Parse(v, ConversionFormatProvider), [typeof(float)] = v => float.Parse(v, FormatProvider),
[typeof(double)] = v => double.Parse(v, ConversionFormatProvider), [typeof(double)] = v => double.Parse(v, FormatProvider),
[typeof(decimal)] = v => decimal.Parse(v, ConversionFormatProvider), [typeof(decimal)] = v => decimal.Parse(v, FormatProvider),
[typeof(DateTime)] = v => DateTime.Parse(v, ConversionFormatProvider), [typeof(DateTime)] = v => DateTime.Parse(v, FormatProvider),
[typeof(DateTimeOffset)] = v => DateTimeOffset.Parse(v, ConversionFormatProvider), [typeof(DateTimeOffset)] = v => DateTimeOffset.Parse(v, FormatProvider),
[typeof(TimeSpan)] = v => TimeSpan.Parse(v, ConversionFormatProvider), [typeof(TimeSpan)] = v => TimeSpan.Parse(v, FormatProvider),
}; };
private static ConstructorInfo? GetStringConstructor(Type type) =>
type.GetConstructor(new[] {typeof(string)});
private static MethodInfo? GetStaticParseMethod(Type type) =>
type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string)}, null);
private static MethodInfo? GetStaticParseMethodWithFormatProvider(Type type) =>
type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string), typeof(IFormatProvider)}, null);
}
// Default and valid value handling.
internal partial class CommandArgumentSchema
{
/// <summary>
/// Retrieves the valid values of this command argument.
/// </summary>
/// <returns>A string collection of this command's valid values.</returns>
public IReadOnlyList<string> GetValidValues()
{
var result = new List<string>();
// Some arguments may have this as null due to a hack that enables built-in options
if (Property == null)
return result;
var underlyingPropertyType =
Property.PropertyType.GetNullableUnderlyingType() ?? Property.PropertyType;
// Enum
if (underlyingPropertyType.IsEnum)
result.AddRange(Enum.GetNames(underlyingPropertyType));
return result;
}
/// <summary>
/// Gets the default value of this command argument.
/// Returns null if there's no default value.
/// </summary>
/// <param name="instance">A dummy instance of the command
/// this command argument belongs to.</param>
/// <returns>The string representation of the default value.
/// If there's no default value, it returns null.</returns>
/// <remarks>
/// We need a dummy instance in order to implement this because
/// we cannot retrieve it from a PropertyInfo.
/// </remarks>
public string? GetDefaultValue(ICommand? instance)
{
if (Property is null || instance is null)
{
return null;
}
var propertyName = Property?.Name;
string? defaultValue = null;
// Get the current culture so that the default value string
// matches the user's culture for cultured information like
// DateTimes and TimeSpans.
var culture = CultureInfo.CurrentCulture;
if (!string.IsNullOrWhiteSpace(propertyName))
{
var instanceProperty = instance.GetType().GetProperty(propertyName);
var value = instanceProperty.GetValue(instance);
if (value.OverridesToStringMethod())
{
// Wrap empty or whitespace strings in quotes so that they're not
// just an ugly blank in the output.
defaultValue = value.ToCulturedString(culture)
.WrapWithQuotesIfEmptyOrWhiteSpace();
}
else if (value is IEnumerable values)
{
// Cast 'values' to IEnumerable<object> so we can use LINQ on it.
defaultValue =
string.Join(" ",
values.Cast<object>()
.Where(v => v != null)
.Select(v => v.ToCulturedString(culture)
.WrapWithQuotesIfEmptyOrWhiteSpace()));
}
}
return defaultValue;
}
} }
} }

View File

@@ -10,10 +10,7 @@ namespace CliFx.Domain
public bool IsPreviewDirective => string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase); public bool IsPreviewDirective => string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase);
public CommandDirectiveInput(string name) public CommandDirectiveInput(string name) => Name = name;
{
Name = name;
}
public override string ToString() => $"[{Name}]"; public override string ToString() => $"[{Name}]";
} }

View File

@@ -8,10 +8,9 @@ namespace CliFx.Domain
{ {
public string Alias { get; } public string Alias { get; }
public string DisplayAlias => public string RawAlias => Alias.Length > 1
Alias.Length > 1 ? $"--{Alias}"
? $"--{Alias}" : $"-{Alias}";
: $"-{Alias}";
public IReadOnlyList<string> Values { get; } public IReadOnlyList<string> Values { get; }
@@ -29,7 +28,7 @@ namespace CliFx.Domain
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
buffer.Append(DisplayAlias); buffer.Append(RawAlias);
foreach (var value in Values) foreach (var value in Values)
{ {

View File

@@ -12,10 +12,6 @@ namespace CliFx.Domain
public char? ShortName { get; } public char? ShortName { get; }
public override string DisplayName => !string.IsNullOrWhiteSpace(Name)
? $"--{Name}"
: $"-{ShortName}";
public string? EnvironmentVariableName { get; } public string? EnvironmentVariableName { get; }
public bool IsRequired { get; } public bool IsRequired { get; }
@@ -51,27 +47,35 @@ namespace CliFx.Domain
!string.IsNullOrWhiteSpace(EnvironmentVariableName) && !string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase); string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase);
public override string ToString() public string GetUserFacingDisplayString()
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name)) if (!string.IsNullOrWhiteSpace(Name))
{ {
buffer.Append("--"); buffer
buffer.Append(Name); .Append("--")
.Append(Name);
} }
if (!string.IsNullOrWhiteSpace(Name) && ShortName != null) if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
{
buffer.Append('|'); buffer.Append('|');
}
if (ShortName != null) if (ShortName != null)
{ {
buffer.Append('-'); buffer
buffer.Append(ShortName); .Append('-')
.Append(ShortName);
} }
return buffer.ToString(); return buffer.ToString();
} }
public string GetInternalDisplayString() => $"{Property.Name} ('{GetUserFacingDisplayString()}')";
public override string ToString() => GetInternalDisplayString();
} }
internal partial class CommandOptionSchema internal partial class CommandOptionSchema

View File

@@ -8,31 +8,30 @@ namespace CliFx.Domain
{ {
public int Order { get; } public int Order { get; }
public string? Name { get; } public string Name { get; }
public override string DisplayName => public CommandParameterSchema(PropertyInfo property, int order, string name, string? description)
!string.IsNullOrWhiteSpace(Name)
? Name
: Property.Name.ToLowerInvariant();
public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description)
: base(property, description) : base(property, description)
{ {
Order = order; Order = order;
Name = name; Name = name;
} }
public override string ToString() public string GetUserFacingDisplayString()
{ {
var buffer = new StringBuilder(); var buffer = new StringBuilder();
buffer buffer
.Append('<') .Append('<')
.Append(DisplayName) .Append(Name)
.Append('>'); .Append('>');
return buffer.ToString(); return buffer.ToString();
} }
public string GetInternalDisplayString() => $"{Property.Name} ([{Order}] {GetUserFacingDisplayString()})";
public override string ToString() => GetInternalDisplayString();
} }
internal partial class CommandParameterSchema internal partial class CommandParameterSchema
@@ -43,10 +42,12 @@ namespace CliFx.Domain
if (attribute == null) if (attribute == null)
return null; return null;
var name = attribute.Name ?? property.Name.ToLowerInvariant();
return new CommandParameterSchema( return new CommandParameterSchema(
property, property,
attribute.Order, attribute.Order,
attribute.Name, name,
attribute.Description attribute.Description
); );
} }

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
@@ -173,27 +173,11 @@ namespace CliFx.Domain
return command; return command;
} }
public override string ToString() public string GetUserFacingDisplayString() => Name ?? "";
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(Name)) public string GetInternalDisplayString() => $"{Type.FullName} ('{GetUserFacingDisplayString()}')";
buffer.Append(Name);
foreach (var parameter in Parameters) public override string ToString() => GetInternalDisplayString();
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(parameter);
}
foreach (var option in Options)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(option);
}
return buffer.ToString();
}
} }
internal partial class CommandSchema internal partial class CommandSchema
@@ -233,7 +217,13 @@ namespace CliFx.Domain
internal partial class CommandSchema internal partial class CommandSchema
{ {
public static CommandSchema StubDefaultCommand { get; } = // TODO: won't work with dep injection
new CommandSchema(null!, null, null, new CommandParameterSchema[0], new CommandOptionSchema[0]); [Command]
public class StubDefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
public static CommandSchema Schema { get; } = TryResolve(typeof(StubDefaultCommand))!;
}
} }
} }

View File

@@ -4,10 +4,7 @@
{ {
public string Value { get; } public string Value { get; }
public CommandUnboundArgumentInput(string value) public CommandUnboundArgumentInput(string value) => Value = value;
{
Value = value;
}
public override string ToString() => Value; public override string ToString() => Value;
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using CliFx.Internal; using CliFx.Internal;
@@ -8,353 +9,342 @@ namespace CliFx.Domain
{ {
private readonly ApplicationMetadata _metadata; private readonly ApplicationMetadata _metadata;
private readonly IConsole _console; private readonly IConsole _console;
private readonly ITypeActivator _typeActivator;
public HelpTextWriter(ApplicationMetadata metadata, IConsole console) private int _column;
private int _row;
private bool IsEmpty => _column == 0 && _row == 0;
public HelpTextWriter(ApplicationMetadata metadata, IConsole console, ITypeActivator typeActivator)
{ {
_metadata = metadata; _metadata = metadata;
_console = console; _console = console;
_typeActivator = typeActivator;
} }
public void Write(ApplicationSchema applicationSchema, CommandSchema command) private void Write(char value)
{ {
var column = 0; _console.Output.Write(value);
var row = 0; _column++;
}
var childCommands = applicationSchema.GetChildCommands(command.Name); private void Write(string value)
{
_console.Output.Write(value);
_column += value.Length;
}
bool IsEmpty() => column == 0 && row == 0; private void Write(ConsoleColor foregroundColor, string value)
{
_console.WithForegroundColor(foregroundColor, () => Write(value));
}
void Render(string text) private void WriteLine()
{
_console.Output.WriteLine();
_column = 0;
_row++;
}
private void WriteVerticalMargin(int size = 1)
{
if (IsEmpty)
return;
for (var i = 0; i < size; i++)
WriteLine();
}
private void WriteHorizontalMargin(int size = 2)
{
if (IsEmpty)
return;
for (var i = 0; i < size; i++)
Write(' ');
}
private void WriteHorizontalColumnMargin(int columnSize = 20, int offsetSize = 2)
{
if (_column + offsetSize < columnSize)
WriteHorizontalMargin(columnSize - _column);
else
WriteHorizontalMargin(offsetSize);
}
private void WriteHeader(string text)
{
Write(ConsoleColor.Magenta, text);
WriteLine();
}
private void WriteApplicationInfo(CommandSchema commandSchema)
{
if (!commandSchema.IsDefault)
return;
// Title and version
Write(ConsoleColor.Yellow, _metadata.Title);
Write(' ');
Write(ConsoleColor.Yellow, _metadata.VersionText);
WriteLine();
// Description
if (!string.IsNullOrWhiteSpace(_metadata.Description))
{ {
_console.Output.Write(text); WriteHorizontalMargin();
Write(_metadata.Description);
WriteLine();
}
}
column += text.Length; private void WriteCommandDescription(CommandSchema commandSchema)
{
if (string.IsNullOrWhiteSpace(commandSchema.Description))
return;
WriteVerticalMargin();
WriteHeader("Description");
WriteHorizontalMargin();
Write(commandSchema.Description);
WriteLine();
}
private void WriteCommandUsage(
CommandSchema commandSchema,
IReadOnlyList<CommandSchema> childCommandSchemas)
{
WriteVerticalMargin();
WriteHeader("Usage");
// Exe name
WriteHorizontalMargin();
Write(_metadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(commandSchema.Name))
{
Write(' ');
Write(ConsoleColor.Cyan, commandSchema.Name);
} }
void RenderNewLine() // Child command placeholder
if (childCommandSchemas.Any())
{ {
_console.Output.WriteLine(); Write(' ');
Write(ConsoleColor.Cyan, "[command]");
column = 0;
row++;
} }
void RenderMargin(int lines = 1) // Parameters
foreach (var parameterSchema in commandSchema.Parameters)
{ {
if (!IsEmpty()) Write(' ');
Write(parameterSchema.IsScalar
? $"<{parameterSchema.Name}>"
: $"<{parameterSchema.Name}...>"
);
}
// Required options
foreach (var optionSchema in commandSchema.Options.Where(o => o.IsRequired))
{
Write(' ');
Write(ConsoleColor.White, !string.IsNullOrWhiteSpace(optionSchema.Name)
? $"--{optionSchema.Name}"
: $"-{optionSchema.ShortName}"
);
Write(' ');
Write(optionSchema.IsScalar
? "<value>"
: "<values...>"
);
}
// Options placeholder
Write(' ');
Write(ConsoleColor.White, "[options]");
WriteLine();
}
private void WriteCommandParameters(CommandSchema commandSchema)
{
if (!commandSchema.Parameters.Any())
return;
WriteVerticalMargin();
WriteHeader("Parameters");
foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order))
{
Write(ConsoleColor.Red, "* ");
Write(ConsoleColor.White, $"{parameterSchema.Name}");
WriteHorizontalColumnMargin();
// Description
if (!string.IsNullOrWhiteSpace(parameterSchema.Description))
{ {
for (var i = 0; i < lines; i++) Write(parameterSchema.Description);
RenderNewLine(); Write(' ');
} }
}
void RenderIndent(int spaces = 2) // Valid values
{ var validValues = parameterSchema.GetValidValues();
Render(' '.Repeat(spaces)); if (validValues.Any())
}
void RenderColumnIndent(int spaces = 20, int margin = 2)
{
if (column + margin < spaces)
{ {
RenderIndent(spaces - column); Write($"Valid values: {string.Join(", ", validValues)}.");
}
WriteLine();
}
}
private void WriteCommandOptions(CommandSchema commandSchema, ICommand command)
{
WriteVerticalMargin();
WriteHeader("Options");
var actualOptionSchemas = commandSchema.Options
.OrderByDescending(o => o.IsRequired)
.Concat(commandSchema.GetBuiltInOptions());
foreach (var optionSchema in actualOptionSchemas)
{
if (optionSchema.IsRequired)
{
Write(ConsoleColor.Red, "* ");
} }
else else
{ {
RenderIndent(margin); WriteHorizontalMargin();
} }
}
void RenderWithColor(string text, ConsoleColor foregroundColor) // Short name
{ if (optionSchema.ShortName != null)
_console.WithForegroundColor(foregroundColor, () => Render(text)); {
} Write(ConsoleColor.White, $"-{optionSchema.ShortName}");
}
void RenderHeader(string text) // Delimiter
{ if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName != null)
RenderWithColor(text, ConsoleColor.Magenta); {
RenderNewLine(); Write('|');
} }
void RenderApplicationInfo() // Name
{ if (!string.IsNullOrWhiteSpace(optionSchema.Name))
if (!command.IsDefault) {
return; Write(ConsoleColor.White, $"--{optionSchema.Name}");
}
// Title and version WriteHorizontalColumnMargin();
RenderWithColor(_metadata.Title, ConsoleColor.Yellow);
Render(" ");
RenderWithColor(_metadata.VersionText, ConsoleColor.Yellow);
RenderNewLine();
// Description // Description
if (!string.IsNullOrWhiteSpace(_metadata.Description)) if (!string.IsNullOrWhiteSpace(optionSchema.Description))
{ {
Render(_metadata.Description); Write(optionSchema.Description);
RenderNewLine(); Write(' ');
} }
}
void RenderDescription() // Valid values
var validValues = optionSchema.GetValidValues();
if (validValues.Any())
{
Write($"Valid values: {validValues.Select(v => v.Quote()).JoinToString(", ")}.");
Write(' ');
}
// Environment variable
if (!string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
{
Write($"Environment variable: \"{optionSchema.EnvironmentVariableName}\".");
Write(' ');
}
// Default value
if (!optionSchema.IsRequired)
{
// TODO: move quoting logic here?
var defaultValue = optionSchema.TryGetDefaultValue(command);
if (defaultValue != null)
{
Write($"Default: {defaultValue}.");
}
}
WriteLine();
}
}
private void WriteCommandChildren(
CommandSchema commandSchema,
IReadOnlyList<CommandSchema> childCommandSchemas)
{
if (!childCommandSchemas.Any())
return;
WriteVerticalMargin();
WriteHeader("Commands");
foreach (var childCommandSchema in childCommandSchemas)
{ {
if (string.IsNullOrWhiteSpace(command.Description)) var relativeCommandName = !string.IsNullOrWhiteSpace(commandSchema.Name)
return; ? childCommandSchema.Name!.Substring(commandSchema.Name.Length + 1)
: childCommandSchema.Name!;
RenderMargin(); // Name
RenderHeader("Description"); WriteHorizontalMargin();
Write(ConsoleColor.Cyan, relativeCommandName);
RenderIndent(); // Description
Render(command.Description); if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
RenderNewLine(); {
WriteHorizontalColumnMargin();
Write(childCommandSchema.Description);
}
WriteLine();
} }
void RenderUsage() // Child command help tip
WriteVerticalMargin();
Write("You can run `");
Write(_metadata.ExecutableName);
if (!string.IsNullOrWhiteSpace(commandSchema.Name))
{ {
RenderMargin(); Write(' ');
RenderHeader("Usage"); Write(ConsoleColor.Cyan, commandSchema.Name);
// Exe name
RenderIndent();
Render(_metadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(command.Name))
{
Render(" ");
RenderWithColor(command.Name, ConsoleColor.Cyan);
}
// Child command placeholder
if (childCommands.Any())
{
Render(" ");
RenderWithColor("[command]", ConsoleColor.Cyan);
}
// Parameters
foreach (var parameter in command.Parameters)
{
Render(" ");
Render(parameter.IsScalar
? $"<{parameter.DisplayName}>"
: $"<{parameter.DisplayName}...>");
}
// Required options
var requiredOptionSchemas = command.Options
.Where(o => o.IsRequired)
.ToArray();
foreach (var option in requiredOptionSchemas)
{
Render(" ");
if (!string.IsNullOrWhiteSpace(option.Name))
{
RenderWithColor($"--{option.Name}", ConsoleColor.White);
Render(" ");
Render(option.IsScalar
? "<value>"
: "<values...>");
}
else
{
RenderWithColor($"-{option.ShortName}", ConsoleColor.White);
Render(" ");
Render(option.IsScalar
? "<value>"
: "<values...>");
}
}
// Options placeholder
if (command.Options.Count != requiredOptionSchemas.Length)
{
Render(" ");
RenderWithColor("[options]", ConsoleColor.White);
}
RenderNewLine();
} }
void RenderParameters() Write(' ');
{ Write(ConsoleColor.Cyan, "[command]");
if (!command.Parameters.Any())
return;
RenderMargin(); Write(' ');
RenderHeader("Parameters"); Write(ConsoleColor.White, "--help");
var parameters = command.Parameters Write("` to show help on a specific command.");
.OrderBy(p => p.Order)
.ToArray();
foreach (var parameter in parameters) WriteLine();
{ }
RenderWithColor("* ", ConsoleColor.Red);
RenderWithColor($"{parameter.DisplayName}", ConsoleColor.White);
RenderColumnIndent(); public void Write(ApplicationSchema applicationSchema, CommandSchema commandSchema)
{
// Description var childCommandSchemas = applicationSchema.GetChildCommands(commandSchema.Name);
if (!string.IsNullOrWhiteSpace(parameter.Description)) var command = (ICommand) _typeActivator.CreateInstance(commandSchema.Type);
{
Render(parameter.Description);
Render(" ");
}
// Valid values
var validValues = parameter.GetValidValues();
if (validValues.Any())
{
Render($"Valid values: {string.Join(", ", validValues)}.");
Render(" ");
}
RenderNewLine();
}
}
void RenderOptions()
{
RenderMargin();
RenderHeader("Options");
// Instantiate a temporary instance of the command so we can get default values from it.
ICommand? tempInstance = command.Type is null ? null : Activator.CreateInstance(command.Type) as ICommand;
var options = command.Options
.OrderByDescending(o => o.IsRequired)
.Concat(command.GetBuiltInOptions())
.ToArray();
foreach (var option in options)
{
if (option.IsRequired)
{
RenderWithColor("* ", ConsoleColor.Red);
}
else
{
RenderIndent();
}
// Short name
if (option.ShortName != null)
{
RenderWithColor($"-{option.ShortName}", ConsoleColor.White);
}
// Delimiter
if (!string.IsNullOrWhiteSpace(option.Name) && option.ShortName != null)
{
Render("|");
}
// Name
if (!string.IsNullOrWhiteSpace(option.Name))
{
RenderWithColor($"--{option.Name}", ConsoleColor.White);
}
RenderColumnIndent();
// Description
if (!string.IsNullOrWhiteSpace(option.Description))
{
Render(option.Description);
Render(" ");
}
// Valid values
var validValues = option.GetValidValues();
if (validValues.Any())
{
Render($"Valid values: {string.Join(", ", validValues)}.");
Render(" ");
}
// Environment variable
if (!string.IsNullOrWhiteSpace(option.EnvironmentVariableName))
{
Render($"(Environment variable: {option.EnvironmentVariableName})");
Render(" ");
}
// Default value
if (!option.IsRequired)
{
var defaultValue = option.GetDefaultValue(tempInstance);
// If 'defaultValue' is null, it means there's no default value.
if (defaultValue is object)
{
Render($"(Default: {defaultValue})");
Render(" ");
}
}
RenderNewLine();
}
}
void RenderChildCommands()
{
if (!childCommands.Any())
return;
RenderMargin();
RenderHeader("Commands");
foreach (var childCommand in childCommands)
{
var relativeCommandName =
!string.IsNullOrWhiteSpace(command.Name)
? childCommand.Name!.Substring(command.Name.Length + 1)
: childCommand.Name!;
// Name
RenderIndent();
RenderWithColor(relativeCommandName, ConsoleColor.Cyan);
// Description
if (!string.IsNullOrWhiteSpace(childCommand.Description))
{
RenderColumnIndent();
Render(childCommand.Description);
}
RenderNewLine();
}
RenderMargin();
// Child command help tip
Render("You can run `");
Render(_metadata.ExecutableName);
if (!string.IsNullOrWhiteSpace(command.Name))
{
Render(" ");
RenderWithColor(command.Name, ConsoleColor.Cyan);
}
Render(" ");
RenderWithColor("[command]", ConsoleColor.Cyan);
Render(" ");
RenderWithColor("--help", ConsoleColor.White);
Render("` to show help on a specific command.");
RenderNewLine();
}
_console.ResetColor(); _console.ResetColor();
RenderApplicationInfo();
RenderDescription(); WriteApplicationInfo(commandSchema);
RenderUsage(); WriteCommandDescription(commandSchema);
RenderParameters(); WriteCommandUsage(commandSchema, childCommandSchemas);
RenderOptions(); WriteCommandParameters(commandSchema);
RenderChildCommands(); WriteCommandOptions(commandSchema, command);
WriteCommandChildren(commandSchema, childCommandSchemas);
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Domain; using CliFx.Domain;
using CliFx.Internal;
namespace CliFx.Exceptions namespace CliFx.Exceptions
{ {
@@ -35,14 +36,6 @@ namespace CliFx.Exceptions
/// </summary> /// </summary>
public int ExitCode { get; } public int ExitCode { get; }
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(string? message, bool showHelp = false)
: this(message, null, showHelp: showHelp)
{
}
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CliFxException"/>. /// Initializes an instance of <see cref="CliFxException"/>.
/// </summary> /// </summary>
@@ -52,9 +45,18 @@ namespace CliFx.Exceptions
ExitCode = exitCode != 0 ExitCode = exitCode != 0
? exitCode ? exitCode
: throw new ArgumentException("Exit code must not be zero in order to signify failure."); : throw new ArgumentException("Exit code must not be zero in order to signify failure.");
HasMessage = !string.IsNullOrWhiteSpace(message); HasMessage = !string.IsNullOrWhiteSpace(message);
ShowHelp = showHelp; ShowHelp = showHelp;
} }
/// <summary>
/// Initializes an instance of <see cref="CliFxException"/>.
/// </summary>
public CliFxException(string? message, bool showHelp = false)
: this(message, null, showHelp: showHelp)
{
}
} }
// Mid-user-facing exceptions // Mid-user-facing exceptions
@@ -75,7 +77,7 @@ Refer to the readme to learn how to integrate a dependency container of your cho
return new CliFxException(message.Trim(), innerException); return new CliFxException(message.Trim(), innerException);
} }
internal static CliFxException DelegateActivatorReceivedNull(Type type) internal static CliFxException DelegateActivatorReturnedNull(Type type)
{ {
var message = $@" var message = $@"
Failed to create an instance of type '{type.FullName}', received <null> instead. Failed to create an instance of type '{type.FullName}', received <null> instead.
@@ -112,12 +114,11 @@ If you're experiencing problems, please refer to the readme for a quickstart exa
return new CliFxException(message.Trim()); return new CliFxException(message.Trim());
} }
internal static CliFxException CommandsTooManyDefaults( internal static CliFxException CommandsTooManyDefaults(IReadOnlyList<CommandSchema> invalidCommandSchemas)
IReadOnlyList<CommandSchema> invalidCommands)
{ {
var message = $@" var message = $@"
Application configuration is invalid because there are {invalidCommands.Count} default commands: Application configuration is invalid because there are {invalidCommandSchemas.Count} default commands:
{string.Join(Environment.NewLine, invalidCommands.Select(p => p.Type.FullName))} {invalidCommandSchemas.JoinToString(Environment.NewLine)}
There can only be one default command (i.e. command with no name) in an application. 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."; Other commands must have unique non-empty names that identify them.";
@@ -127,11 +128,11 @@ Other commands must have unique non-empty names that identify them.";
internal static CliFxException CommandsDuplicateName( internal static CliFxException CommandsDuplicateName(
string name, string name,
IReadOnlyList<CommandSchema> invalidCommands) IReadOnlyList<CommandSchema> invalidCommandSchemas)
{ {
var message = $@" var message = $@"
Application configuration is invalid because there are {invalidCommands.Count} commands with the same name ('{name}'): Application configuration is invalid because there are {invalidCommandSchemas.Count} commands with the same name ('{name}'):
{string.Join(Environment.NewLine, invalidCommands.Select(p => p.Type.FullName))} {invalidCommandSchemas.JoinToString(Environment.NewLine)}
Commands must have unique names. Commands must have unique names.
Names are not case-sensitive."; Names are not case-sensitive.";
@@ -140,13 +141,13 @@ Names are not case-sensitive.";
} }
internal static CliFxException CommandParametersDuplicateOrder( internal static CliFxException CommandParametersDuplicateOrder(
CommandSchema command, CommandSchema commandSchema,
int order, int order,
IReadOnlyList<CommandParameterSchema> invalidParameters) IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same order ({order}): Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same order ({order}):
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))} {invalidParameterSchemas.JoinToString(Environment.NewLine)}
Parameters must have unique order."; Parameters must have unique order.";
@@ -154,13 +155,13 @@ Parameters must have unique order.";
} }
internal static CliFxException CommandParametersDuplicateName( internal static CliFxException CommandParametersDuplicateName(
CommandSchema command, CommandSchema commandSchema,
string name, string name,
IReadOnlyList<CommandParameterSchema> invalidParameters) IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same name ('{name}'): Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same name ('{name}'):
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))} {invalidParameterSchemas.JoinToString(Environment.NewLine)}
Parameters must have unique names to avoid potential confusion in the help text. Parameters must have unique names to avoid potential confusion in the help text.
Names are not case-sensitive."; Names are not case-sensitive.";
@@ -169,12 +170,12 @@ Names are not case-sensitive.";
} }
internal static CliFxException CommandParametersTooManyNonScalar( internal static CliFxException CommandParametersTooManyNonScalar(
CommandSchema command, CommandSchema commandSchema,
IReadOnlyList<CommandParameterSchema> invalidParameters) IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} non-scalar parameters: Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} non-scalar parameters:
{string.Join(Environment.NewLine, invalidParameters.Select(p => p.Property.Name))} {invalidParameterSchemas.JoinToString(Environment.NewLine)}
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object). 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. Only one parameter in a command may be non-scalar and it must be the last one in order.
@@ -185,12 +186,12 @@ If it's not feasible to fit into these constraints, consider using options inste
} }
internal static CliFxException CommandParametersNonLastNonScalar( internal static CliFxException CommandParametersNonLastNonScalar(
CommandSchema command, CommandSchema commandSchema,
CommandParameterSchema invalidParameter) CommandParameterSchema invalidParameterSchema)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order: Command '{commandSchema.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order:
{invalidParameter.Property.Name} {invalidParameterSchema}
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object). 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. Only one parameter in a command may be non-scalar and it must be the last one in order.
@@ -201,12 +202,12 @@ If it's not feasible to fit into these constraints, consider using options inste
} }
internal static CliFxException CommandOptionsNoName( internal static CliFxException CommandOptionsNoName(
CommandSchema command, CommandSchema commandSchema,
IReadOnlyList<CommandOptionSchema> invalidOptions) IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains one or more options without a name: Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options without a name:
{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} {invalidOptionSchemas.JoinToString(Environment.NewLine)}
Options must have either a name or a short name or both."; Options must have either a name or a short name or both.";
@@ -214,12 +215,12 @@ Options must have either a name or a short name or both.";
} }
internal static CliFxException CommandOptionsInvalidLengthName( internal static CliFxException CommandOptionsInvalidLengthName(
CommandSchema command, CommandSchema commandSchema,
IReadOnlyList<CommandOptionSchema> invalidOptions) IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains one or more options whose names are too short: Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options whose names are too short:
{string.Join(Environment.NewLine, invalidOptions.Select(o => $"{o.Property.Name} ('{o.DisplayName}')"))} {invalidOptionSchemas.JoinToString(Environment.NewLine)}
Option names must be at least 2 characters long to avoid confusion with short names. 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 attribute overload that accepts a char."; If you intended to set the short name instead, use the attribute overload that accepts a char.";
@@ -228,30 +229,28 @@ If you intended to set the short name instead, use the attribute overload that a
} }
internal static CliFxException CommandOptionsDuplicateName( internal static CliFxException CommandOptionsDuplicateName(
CommandSchema command, CommandSchema commandSchema,
string name, string name,
IReadOnlyList<CommandOptionSchema> invalidOptions) IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same name ('{name}'): Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same name ('{name}'):
{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} {invalidOptionSchemas.JoinToString(Environment.NewLine)}
Options must have unique names, because that's what identifies them. Options must have unique names.
Names are not case-sensitive. Names are not case-sensitive.";
To fix this, ensure that all options have different names.";
return new CliFxException(message.Trim()); return new CliFxException(message.Trim());
} }
internal static CliFxException CommandOptionsDuplicateShortName( internal static CliFxException CommandOptionsDuplicateShortName(
CommandSchema command, CommandSchema commandSchema,
char shortName, char shortName,
IReadOnlyList<CommandOptionSchema> invalidOptions) IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same short name ('{shortName}'): Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same short name ('{shortName}'):
{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} {invalidOptionSchemas.JoinToString(Environment.NewLine)}
Options must have unique short names. Options must have unique short names.
Short names are case-sensitive (i.e. 'a' and 'A' are different short names)."; Short names are case-sensitive (i.e. 'a' and 'A' are different short names).";
@@ -260,13 +259,13 @@ Short names are case-sensitive (i.e. 'a' and 'A' are different short names).";
} }
internal static CliFxException CommandOptionsDuplicateEnvironmentVariableName( internal static CliFxException CommandOptionsDuplicateEnvironmentVariableName(
CommandSchema command, CommandSchema commandSchema,
string environmentVariableName, string environmentVariableName,
IReadOnlyList<CommandOptionSchema> invalidOptions) IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
{ {
var message = $@" var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same fallback environment variable name ('{environmentVariableName}'): Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same fallback environment variable name ('{environmentVariableName}'):
{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} {invalidOptionSchemas.JoinToString(Environment.NewLine)}
Options cannot share the same environment variable as a fallback. Options cannot share the same environment variable as a fallback.
Environment variable names are not case-sensitive."; Environment variable names are not case-sensitive.";
@@ -283,92 +282,148 @@ Environment variable names are not case-sensitive.";
{ {
var message = $@" var message = $@"
Can't find a command that matches the following arguments: Can't find a command that matches the following arguments:
{string.Join(" ", input.UnboundArguments.Select(a => a.Value))}"; {input.UnboundArguments.JoinToString(" ")}";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException CannotConvertMultipleValuesToNonScalar( internal static CliFxException CannotConvertMultipleValuesToNonScalar(
CommandArgumentSchema argument, CommandParameterSchema parameterSchema,
IReadOnlyList<string> values) IReadOnlyList<string> values)
{ {
var argumentDisplayText = argument is CommandParameterSchema
? $"Parameter <{argument.DisplayName}>"
: $"Option '{argument.DisplayName}'";
var message = $@" var message = $@"
{argumentDisplayText} expects a single value, but provided with multiple: Parameter {parameterSchema.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
{string.Join(", ", values.Select(v => $"'{v}'"))}"; {values.Select(v => v.Quote()).JoinToString(" ")}";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
CommandOptionSchema optionSchema,
IReadOnlyList<string> values)
{
var message = $@"
Option {optionSchema.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
{values.Select(v => v.Quote()).JoinToString(" ")}";
return new CliFxException(message.Trim(), showHelp: true);
}
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
CommandArgumentSchema argumentSchema,
IReadOnlyList<string> values) => argumentSchema switch
{
CommandParameterSchema parameterSchema => CannotConvertMultipleValuesToNonScalar(parameterSchema, values),
CommandOptionSchema optionSchema => CannotConvertMultipleValuesToNonScalar(optionSchema, values),
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
};
internal static CliFxException CannotConvertToType( internal static CliFxException CannotConvertToType(
CommandArgumentSchema argument, CommandParameterSchema parameterSchema,
string? value, string? value,
Type type, Type type,
Exception? innerException = null) Exception? innerException = null)
{ {
var argumentDisplayText = argument is CommandParameterSchema
? $"parameter <{argument.DisplayName}>"
: $"option '{argument.DisplayName}'";
var message = $@" var message = $@"
Can't convert value '{value ?? "<null>"}' to type '{type.FullName}' for {argumentDisplayText}. Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for parameter {parameterSchema.GetUserFacingDisplayString()}.
{innerException?.Message ?? "This type is not supported."}"; {innerException?.Message ?? "This type is not supported."}";
return new CliFxException(message.Trim(), innerException, showHelp: true); return new CliFxException(message.Trim(), innerException, showHelp: true);
} }
internal static CliFxException CannotConvertToType(
CommandOptionSchema optionSchema,
string? value,
Type type,
Exception? innerException = null)
{
var message = $@"
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for option {optionSchema.GetUserFacingDisplayString()}.
{innerException?.Message ?? "This type is not supported."}";
return new CliFxException(message.Trim(), innerException, showHelp: true);
}
internal static CliFxException CannotConvertToType(
CommandArgumentSchema argumentSchema,
string? value,
Type type,
Exception? innerException = null) => argumentSchema switch
{
CommandParameterSchema parameterSchema => CannotConvertToType(parameterSchema, value, type, innerException),
CommandOptionSchema optionSchema => CannotConvertToType(optionSchema, value, type, innerException),
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
};
internal static CliFxException CannotConvertNonScalar( internal static CliFxException CannotConvertNonScalar(
CommandArgumentSchema argument, CommandParameterSchema parameterSchema,
IReadOnlyList<string> values, IReadOnlyList<string> values,
Type type) Type type)
{ {
var argumentDisplayText = argument is CommandParameterSchema
? $"parameter <{argument.DisplayName}>"
: $"option '{argument.DisplayName}'";
var message = $@" var message = $@"
Can't convert provided values to type '{type.FullName}' for {argumentDisplayText}: Can't convert provided values to type '{type.Name}' for parameter {parameterSchema.GetUserFacingDisplayString()}:
{string.Join(", ", values.Select(v => $"'{v}'"))} {values.Select(v => v.Quote()).JoinToString(" ")}
Target type is not assignable from array and doesn't have a public constructor that takes an array."; Target type is not assignable from array and doesn't have a public constructor that takes an array.";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException ParameterNotSet(CommandParameterSchema parameter) internal static CliFxException CannotConvertNonScalar(
CommandOptionSchema optionSchema,
IReadOnlyList<string> values,
Type type)
{ {
var message = $@" var message = $@"
Missing value for parameter <{parameter.DisplayName}>."; Can't convert provided values to type '{type.Name}' for option {optionSchema.GetUserFacingDisplayString()}:
{values.Select(v => v.Quote()).JoinToString(" ")}
Target type is not assignable from array and doesn't have a public constructor that takes an array.";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException RequiredOptionsNotSet(IReadOnlyList<CommandOptionSchema> options) internal static CliFxException CannotConvertNonScalar(
CommandArgumentSchema argumentSchema,
IReadOnlyList<string> values,
Type type) => argumentSchema switch
{
CommandParameterSchema parameterSchema => CannotConvertNonScalar(parameterSchema, values, type),
CommandOptionSchema optionSchema => CannotConvertNonScalar(optionSchema, values, type),
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
};
internal static CliFxException ParameterNotSet(CommandParameterSchema parameterSchema)
{
var message = $@"
Missing value for parameter {parameterSchema.GetUserFacingDisplayString()}.";
return new CliFxException(message.Trim(), showHelp: true);
}
internal static CliFxException RequiredOptionsNotSet(IReadOnlyList<CommandOptionSchema> optionSchemas)
{ {
var message = $@" var message = $@"
Missing values for one or more required options: Missing values for one or more required options:
{string.Join(Environment.NewLine, options.Select(o => o.DisplayName))}"; {optionSchemas.Select(o => o.GetUserFacingDisplayString()).JoinToString(Environment.NewLine)}";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> inputs) internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> argumentInputs)
{ {
var message = $@" var message = $@"
Unrecognized parameters provided: Unrecognized parameters provided:
{string.Join(Environment.NewLine, inputs.Select(i => $"<{i.Value}>"))}"; {argumentInputs.Select(a => a.Value.Quote()).JoinToString(" ")}";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }
internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> inputs) internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> optionInputs)
{ {
var message = $@" var message = $@"
Unrecognized options provided: Unrecognized options provided:
{string.Join(Environment.NewLine, inputs.Select(i => i.DisplayAlias))}"; {optionInputs.Select(o => o.RawAlias).JoinToString(Environment.NewLine)}";
return new CliFxException(message.Trim(), showHelp: true); return new CliFxException(message.Trim(), showHelp: true);
} }

View File

@@ -12,18 +12,17 @@ namespace CliFx.Exceptions
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandException"/>. /// Initializes an instance of <see cref="CommandException"/>.
/// </summary> /// </summary>
public CommandException(string? message, Exception? innerException, public CommandException(string? message, Exception? innerException,
int exitCode = DefaultExitCode, bool showHelp = false) int exitCode = DefaultExitCode, bool showHelp = false)
: base(message, innerException, exitCode, showHelp) : base(message, innerException, exitCode, showHelp)
{ {
} }
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandException"/>. /// Initializes an instance of <see cref="CommandException"/>.
/// </summary> /// </summary>
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false) public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
: this(message, null, exitCode, showHelp) : this(message, null, exitCode, showHelp)
{ {
} }

View File

@@ -9,7 +9,7 @@ namespace CliFx
{ {
/// <summary> /// <summary>
/// Executes the command using the specified implementation of <see cref="IConsole"/>. /// Executes the command using the specified implementation of <see cref="IConsole"/>.
/// This is the method that's called when the command is invoked by a user through command line interface. /// This is the method that's called when the command is invoked by a user through command line.
/// </summary> /// </summary>
/// <remarks>If the execution of the command is not asynchronous, simply end the method with <code>return default;</code></remarks> /// <remarks>If the execution of the command is not asynchronous, simply end the method with <code>return default;</code></remarks>
ValueTask ExecuteAsync(IConsole console); ValueTask ExecuteAsync(IConsole console);

View File

@@ -3,12 +3,12 @@
namespace CliFx namespace CliFx
{ {
/// <summary> /// <summary>
/// Abstraction for a service can initialize objects at runtime. /// Abstraction for a service that can initialize objects at runtime.
/// </summary> /// </summary>
public interface ITypeActivator public interface ITypeActivator
{ {
/// <summary> /// <summary>
/// Creates an instance of specified type. /// Creates an instance of the specified type.
/// </summary> /// </summary>
object CreateInstance(Type type); object CreateInstance(Type type);
} }

View File

@@ -0,0 +1,13 @@
using System.Diagnostics;
namespace CliFx.Internal
{
internal static class ProcessEx
{
public static int GetCurrentProcessId()
{
using var process = Process.GetCurrentProcess();
return process.Id;
}
}
}

View File

@@ -1,5 +1,5 @@
using System; using System;
using System.Globalization; using System.Collections.Generic;
using System.Text; using System.Text;
namespace CliFx.Internal namespace CliFx.Internal
@@ -10,14 +10,17 @@ namespace CliFx.Internal
public static string AsString(this char c) => c.Repeat(1); public static string AsString(this char c) => c.Repeat(1);
public static string Quote(this string str) => $"\"{str}\"";
public static string JoinToString<T>(this IEnumerable<T> source, string separator) => string.Join(separator, source);
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 bool IsEmptyOrWhiteSpace(this string s) => s is object && string.IsNullOrWhiteSpace(s); public static string ToFormattableString(this object obj,
IFormatProvider? formatProvider = null, string? format = null) =>
public static string WrapWithQuotesIfEmptyOrWhiteSpace(this string s) => obj is IFormattable formattable
s.IsEmptyOrWhiteSpace() ? $"\"{s}\"" : s; ? formattable.ToString(format, formatProvider)
: obj.ToString();
public static string ToCulturedString(this object obj, CultureInfo culture) => Convert.ToString(obj, culture);
} }
} }

View File

@@ -2,11 +2,17 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
namespace CliFx.Internal namespace CliFx.Internal
{ {
internal static class TypeExtensions internal static class TypeExtensions
{ {
public static object? GetDefaultValue(this Type type) =>
type.IsValueType
? Activator.CreateInstance(type)
: null;
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);
@@ -22,13 +28,30 @@ namespace CliFx.Internal
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return type.GetGenericArguments().FirstOrDefault(); return type.GetGenericArguments().FirstOrDefault();
return type.GetInterfaces() return type
.GetInterfaces()
.Select(GetEnumerableUnderlyingType) .Select(GetEnumerableUnderlyingType)
.Where(t => t != null) .Where(t => t != null)
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types .OrderByDescending(t => t != typeof(object)) // prioritize more specific types
.FirstOrDefault(); .FirstOrDefault();
} }
public static MethodInfo GetToStringMethod(this Type type) => type.GetMethod(nameof(ToString), Type.EmptyTypes);
public static bool IsToStringOverriden(this Type type) => type.GetToStringMethod() != typeof(object).GetToStringMethod();
public static MethodInfo GetStaticParseMethod(this Type type, bool withFormatProvider = false)
{
var argumentTypes = withFormatProvider
? new[] {typeof(string), typeof(IFormatProvider)}
: new[] {typeof(string)};
return type.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static,
null, argumentTypes, null
);
}
public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType) public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType)
{ {
var sourceAsCollection = source as ICollection ?? source.ToArray(); var sourceAsCollection = source as ICollection ?? source.ToArray();
@@ -38,7 +61,5 @@ namespace CliFx.Internal
return array; return array;
} }
public static bool OverridesToStringMethod(this object obj) => obj?.ToString() != obj?.GetType().ToString();
} }
} }