mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Refactor
This commit is contained in:
@@ -51,6 +51,8 @@ namespace CliFx.Tests
|
||||
console.ResetColor();
|
||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
||||
console.CursorLeft = 42;
|
||||
console.CursorTop = 24;
|
||||
|
||||
// Assert
|
||||
stdInData.Should().Be("input");
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
@@ -89,16 +85,16 @@ namespace CliFx.Tests
|
||||
[Command("cmd-with-enum-args")]
|
||||
private class EnumArgumentsCommand : ICommand
|
||||
{
|
||||
public enum TestEnum { Value1, Value2, Value3 };
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandParameter(0, Name = "value", Description = "Enum parameter.")]
|
||||
public TestEnum ParamA { get; set; }
|
||||
public CustomEnum ParamA { get; set; }
|
||||
|
||||
[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.")]
|
||||
public TestEnum? OptionB { get; set; }
|
||||
public CustomEnum? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
@@ -116,8 +112,10 @@ namespace CliFx.Tests
|
||||
}
|
||||
|
||||
[Command("cmd-with-defaults")]
|
||||
private class DefaultArgumentsCommand : ICommand
|
||||
private class ArgumentsWithDefaultValuesCommand : ICommand
|
||||
{
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandOption(nameof(Object))]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
@@ -127,98 +125,29 @@ namespace CliFx.Tests
|
||||
[CommandOption(nameof(EmptyString))]
|
||||
public string EmptyString { get; set; } = "";
|
||||
|
||||
[CommandOption(nameof(WhiteSpaceString))]
|
||||
public string WhiteSpaceString { get; set; } = " ";
|
||||
|
||||
[CommandOption(nameof(Bool))]
|
||||
public bool Bool { get; set; } = true;
|
||||
|
||||
[CommandOption(nameof(Char))]
|
||||
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))]
|
||||
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))]
|
||||
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
|
||||
|
||||
public enum TestEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandOption(nameof(CustomEnum))]
|
||||
public TestEnum CustomEnum { get; set; } = TestEnum.Value2;
|
||||
[CommandOption(nameof(Enum))]
|
||||
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
|
||||
|
||||
[CommandOption(nameof(IntNullable))]
|
||||
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))]
|
||||
public string[]? StringArray { get; set; } = new[] { "foo", "bar", "baz" };
|
||||
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
|
||||
|
||||
[CommandOption(nameof(IntArray))]
|
||||
public int[]? IntArray { get; set; } = new[] { 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 int[]? IntArray { get; set; } = { 1, 2, 3 };
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ namespace CliFx.Tests
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultArgumentsCommand))
|
||||
.AddCommand(typeof(ArgumentsWithDefaultValuesCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
@@ -317,38 +317,17 @@ namespace CliFx.Tests
|
||||
"Usage",
|
||||
"cmd-with-defaults", "[options]",
|
||||
"Options",
|
||||
"--Object", "(Default: 42)",
|
||||
"--String", "(Default: foo)",
|
||||
"--EmptyString", "(Default: \"\"",
|
||||
"--WhiteSpaceString", "(Default: \" \"",
|
||||
"--Bool", "(Default: True)",
|
||||
"--Char", "(Default: t)",
|
||||
"--Sbyte", "(Default: -3)",
|
||||
"--Byte", "(Default: 3)",
|
||||
"--Short", "(Default: -1234)",
|
||||
"--Ushort", "(Default: 1234)",
|
||||
"--Int", "(Default: 1337)",
|
||||
"--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)"
|
||||
"--Object", "Default: \"42\"",
|
||||
"--String", "Default: \"foo\"",
|
||||
"--EmptyString", "Default: \"\"",
|
||||
"--Bool", "Default: \"True\"",
|
||||
"--Char", "Default: \"t\"",
|
||||
"--Int", "Default: \"1337\"",
|
||||
"--TimeSpan", "Default: \"02:03:00\"",
|
||||
"--Enum", "Default: \"Value2\"",
|
||||
"--IntNullable", "Default: \"1337\"",
|
||||
"--StringArray", "Default: \"foo\" \"bar\" \"baz\"",
|
||||
"--IntArray", "Default: \"1\" \"2\" \"3\""
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
@@ -33,7 +34,7 @@ namespace CliFx
|
||||
_console = console;
|
||||
_typeActivator = typeActivator;
|
||||
|
||||
_helpTextWriter = new HelpTextWriter(metadata, console);
|
||||
_helpTextWriter = new HelpTextWriter(metadata, console, typeActivator);
|
||||
}
|
||||
|
||||
private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
|
||||
@@ -42,8 +43,10 @@ namespace CliFx
|
||||
if (!isDebugMode)
|
||||
return null;
|
||||
|
||||
var processId = ProcessEx.GetCurrentProcessId();
|
||||
|
||||
_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)
|
||||
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
|
||||
var commandSchema =
|
||||
applicationSchema.TryFindCommand(commandLineInput) ??
|
||||
CommandSchema.StubDefaultCommand;
|
||||
CommandSchema.StubDefaultCommand.Schema;
|
||||
|
||||
_helpTextWriter.Write(applicationSchema, commandSchema);
|
||||
|
||||
@@ -143,17 +146,13 @@ namespace CliFx
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
private int HandleCliFxException(IReadOnlyList<string> commandLineArguments, CliFxException ex)
|
||||
{
|
||||
var showHelp = cfe.ShowHelp;
|
||||
var showHelp = ex.ShowHelp;
|
||||
|
||||
var errorMessage = cfe.HasMessage
|
||||
? cfe.Message
|
||||
: cfe.ToString();
|
||||
var errorMessage = ex.HasMessage
|
||||
? ex.Message
|
||||
: ex.ToString();
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
|
||||
|
||||
@@ -162,11 +161,11 @@ namespace CliFx
|
||||
var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
|
||||
var commandLineInput = CommandLineInput.Parse(commandLineArguments);
|
||||
var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ??
|
||||
CommandSchema.StubDefaultCommand;
|
||||
CommandSchema.StubDefaultCommand.Schema;
|
||||
_helpTextWriter.Write(applicationSchema, commandSchema);
|
||||
}
|
||||
|
||||
return cfe.ExitCode;
|
||||
return ex.ExitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -188,16 +187,15 @@ namespace CliFx
|
||||
HandleHelpOption(applicationSchema, commandLineInput) ??
|
||||
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.
|
||||
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
|
||||
var exitCode = HandleCliFxException(commandLineArguments, cfe);
|
||||
return exitCode;
|
||||
// Some exceptions may specify exit code or request help
|
||||
return HandleCliFxException(commandLineArguments, 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()));
|
||||
return ex.HResult;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ namespace CliFx
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateInstance(Type type) =>
|
||||
_func(type) ?? throw CliFxException.DelegateActivatorReceivedNull(type);
|
||||
_func(type) ?? throw CliFxException.DelegateActivatorReturnedNull(type);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ namespace CliFx.Domain
|
||||
|
||||
public string? Description { get; }
|
||||
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
|
||||
|
||||
protected CommandArgumentSchema(PropertyInfo property, string? description)
|
||||
@@ -51,17 +49,17 @@ namespace CliFx.Domain
|
||||
: null;
|
||||
|
||||
// String-constructable
|
||||
var stringConstructor = GetStringConstructor(targetType);
|
||||
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
|
||||
if (stringConstructor != null)
|
||||
return stringConstructor.Invoke(new object[] {value!});
|
||||
|
||||
// String-parseable (with format provider)
|
||||
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
||||
var parseMethodWithFormatProvider = targetType.GetStaticParseMethod(true);
|
||||
if (parseMethodWithFormatProvider != null)
|
||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, ConversionFormatProvider});
|
||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, FormatProvider});
|
||||
|
||||
// String-parseable (without format provider)
|
||||
var parseMethod = GetStaticParseMethod(targetType);
|
||||
var parseMethod = targetType.GetStaticParseMethod();
|
||||
if (parseMethod != null)
|
||||
return parseMethod.Invoke(null, new object[] {value!});
|
||||
}
|
||||
@@ -117,11 +115,62 @@ namespace CliFx.Domain
|
||||
|
||||
public void Inject(ICommand command, params 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
|
||||
{
|
||||
private static readonly IFormatProvider ConversionFormatProvider = CultureInfo.InvariantCulture;
|
||||
private static readonly IFormatProvider FormatProvider = CultureInfo.InvariantCulture;
|
||||
|
||||
private static readonly IReadOnlyDictionary<Type, Func<string?, object?>> PrimitiveConverters =
|
||||
new Dictionary<Type, Func<string?, object?>>
|
||||
@@ -130,112 +179,20 @@ namespace CliFx.Domain
|
||||
[typeof(string)] = v => v,
|
||||
[typeof(bool)] = v => string.IsNullOrWhiteSpace(v) || bool.Parse(v),
|
||||
[typeof(char)] = v => v.Single(),
|
||||
[typeof(sbyte)] = v => sbyte.Parse(v, ConversionFormatProvider),
|
||||
[typeof(byte)] = v => byte.Parse(v, ConversionFormatProvider),
|
||||
[typeof(short)] = v => short.Parse(v, ConversionFormatProvider),
|
||||
[typeof(ushort)] = v => ushort.Parse(v, ConversionFormatProvider),
|
||||
[typeof(int)] = v => int.Parse(v, ConversionFormatProvider),
|
||||
[typeof(uint)] = v => uint.Parse(v, ConversionFormatProvider),
|
||||
[typeof(long)] = v => long.Parse(v, ConversionFormatProvider),
|
||||
[typeof(ulong)] = v => ulong.Parse(v, ConversionFormatProvider),
|
||||
[typeof(float)] = v => float.Parse(v, ConversionFormatProvider),
|
||||
[typeof(double)] = v => double.Parse(v, ConversionFormatProvider),
|
||||
[typeof(decimal)] = v => decimal.Parse(v, ConversionFormatProvider),
|
||||
[typeof(DateTime)] = v => DateTime.Parse(v, ConversionFormatProvider),
|
||||
[typeof(DateTimeOffset)] = v => DateTimeOffset.Parse(v, ConversionFormatProvider),
|
||||
[typeof(TimeSpan)] = v => TimeSpan.Parse(v, ConversionFormatProvider),
|
||||
[typeof(sbyte)] = v => sbyte.Parse(v, FormatProvider),
|
||||
[typeof(byte)] = v => byte.Parse(v, FormatProvider),
|
||||
[typeof(short)] = v => short.Parse(v, FormatProvider),
|
||||
[typeof(ushort)] = v => ushort.Parse(v, FormatProvider),
|
||||
[typeof(int)] = v => int.Parse(v, FormatProvider),
|
||||
[typeof(uint)] = v => uint.Parse(v, FormatProvider),
|
||||
[typeof(long)] = v => long.Parse(v, FormatProvider),
|
||||
[typeof(ulong)] = v => ulong.Parse(v, FormatProvider),
|
||||
[typeof(float)] = v => float.Parse(v, FormatProvider),
|
||||
[typeof(double)] = v => double.Parse(v, FormatProvider),
|
||||
[typeof(decimal)] = v => decimal.Parse(v, FormatProvider),
|
||||
[typeof(DateTime)] = v => DateTime.Parse(v, FormatProvider),
|
||||
[typeof(DateTimeOffset)] = v => DateTimeOffset.Parse(v, FormatProvider),
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,7 @@ namespace CliFx.Domain
|
||||
|
||||
public bool IsPreviewDirective => string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public CommandDirectiveInput(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
public CommandDirectiveInput(string name) => Name = name;
|
||||
|
||||
public override string ToString() => $"[{Name}]";
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ namespace CliFx.Domain
|
||||
{
|
||||
public string Alias { get; }
|
||||
|
||||
public string DisplayAlias =>
|
||||
Alias.Length > 1
|
||||
? $"--{Alias}"
|
||||
: $"-{Alias}";
|
||||
public string RawAlias => Alias.Length > 1
|
||||
? $"--{Alias}"
|
||||
: $"-{Alias}";
|
||||
|
||||
public IReadOnlyList<string> Values { get; }
|
||||
|
||||
@@ -29,7 +28,7 @@ namespace CliFx.Domain
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer.Append(DisplayAlias);
|
||||
buffer.Append(RawAlias);
|
||||
|
||||
foreach (var value in Values)
|
||||
{
|
||||
|
||||
@@ -12,10 +12,6 @@ namespace CliFx.Domain
|
||||
|
||||
public char? ShortName { get; }
|
||||
|
||||
public override string DisplayName => !string.IsNullOrWhiteSpace(Name)
|
||||
? $"--{Name}"
|
||||
: $"-{ShortName}";
|
||||
|
||||
public string? EnvironmentVariableName { get; }
|
||||
|
||||
public bool IsRequired { get; }
|
||||
@@ -51,27 +47,35 @@ namespace CliFx.Domain
|
||||
!string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
|
||||
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public override string ToString()
|
||||
public string GetUserFacingDisplayString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
buffer.Append("--");
|
||||
buffer.Append(Name);
|
||||
buffer
|
||||
.Append("--")
|
||||
.Append(Name);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name) && ShortName != null)
|
||||
{
|
||||
buffer.Append('|');
|
||||
}
|
||||
|
||||
if (ShortName != null)
|
||||
{
|
||||
buffer.Append('-');
|
||||
buffer.Append(ShortName);
|
||||
buffer
|
||||
.Append('-')
|
||||
.Append(ShortName);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property.Name} ('{GetUserFacingDisplayString()}')";
|
||||
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
internal partial class CommandOptionSchema
|
||||
|
||||
@@ -8,31 +8,30 @@ namespace CliFx.Domain
|
||||
{
|
||||
public int Order { get; }
|
||||
|
||||
public string? Name { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public override string DisplayName =>
|
||||
!string.IsNullOrWhiteSpace(Name)
|
||||
? Name
|
||||
: Property.Name.ToLowerInvariant();
|
||||
|
||||
public CommandParameterSchema(PropertyInfo property, int order, string? name, string? description)
|
||||
public CommandParameterSchema(PropertyInfo property, int order, string name, string? description)
|
||||
: base(property, description)
|
||||
{
|
||||
Order = order;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
public string GetUserFacingDisplayString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
buffer
|
||||
.Append('<')
|
||||
.Append(DisplayName)
|
||||
.Append(Name)
|
||||
.Append('>');
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property.Name} ([{Order}] {GetUserFacingDisplayString()})";
|
||||
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
internal partial class CommandParameterSchema
|
||||
@@ -43,10 +42,12 @@ namespace CliFx.Domain
|
||||
if (attribute == null)
|
||||
return null;
|
||||
|
||||
var name = attribute.Name ?? property.Name.ToLowerInvariant();
|
||||
|
||||
return new CommandParameterSchema(
|
||||
property,
|
||||
attribute.Order,
|
||||
attribute.Name,
|
||||
name,
|
||||
attribute.Description
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
@@ -173,27 +173,11 @@ namespace CliFx.Domain
|
||||
return command;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
public string GetUserFacingDisplayString() => Name ?? "";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
buffer.Append(Name);
|
||||
public string GetInternalDisplayString() => $"{Type.FullName} ('{GetUserFacingDisplayString()}')";
|
||||
|
||||
foreach (var parameter in Parameters)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(parameter);
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(option);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
internal partial class CommandSchema
|
||||
@@ -233,7 +217,13 @@ namespace CliFx.Domain
|
||||
|
||||
internal partial class CommandSchema
|
||||
{
|
||||
public static CommandSchema StubDefaultCommand { get; } =
|
||||
new CommandSchema(null!, null, null, new CommandParameterSchema[0], new CommandOptionSchema[0]);
|
||||
// TODO: won't work with dep injection
|
||||
[Command]
|
||||
public class StubDefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
|
||||
public static CommandSchema Schema { get; } = TryResolve(typeof(StubDefaultCommand))!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,7 @@
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public CommandUnboundArgumentInput(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public CommandUnboundArgumentInput(string value) => Value = value;
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
@@ -8,353 +9,342 @@ namespace CliFx.Domain
|
||||
{
|
||||
private readonly ApplicationMetadata _metadata;
|
||||
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;
|
||||
_console = console;
|
||||
_typeActivator = typeActivator;
|
||||
}
|
||||
|
||||
public void Write(ApplicationSchema applicationSchema, CommandSchema command)
|
||||
private void Write(char value)
|
||||
{
|
||||
var column = 0;
|
||||
var row = 0;
|
||||
_console.Output.Write(value);
|
||||
_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();
|
||||
|
||||
column = 0;
|
||||
row++;
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, "[command]");
|
||||
}
|
||||
|
||||
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++)
|
||||
RenderNewLine();
|
||||
Write(parameterSchema.Description);
|
||||
Write(' ');
|
||||
}
|
||||
}
|
||||
|
||||
void RenderIndent(int spaces = 2)
|
||||
{
|
||||
Render(' '.Repeat(spaces));
|
||||
}
|
||||
|
||||
void RenderColumnIndent(int spaces = 20, int margin = 2)
|
||||
{
|
||||
if (column + margin < spaces)
|
||||
// Valid values
|
||||
var validValues = parameterSchema.GetValidValues();
|
||||
if (validValues.Any())
|
||||
{
|
||||
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
|
||||
{
|
||||
RenderIndent(margin);
|
||||
WriteHorizontalMargin();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderWithColor(string text, ConsoleColor foregroundColor)
|
||||
{
|
||||
_console.WithForegroundColor(foregroundColor, () => Render(text));
|
||||
}
|
||||
// Short name
|
||||
if (optionSchema.ShortName != null)
|
||||
{
|
||||
Write(ConsoleColor.White, $"-{optionSchema.ShortName}");
|
||||
}
|
||||
|
||||
void RenderHeader(string text)
|
||||
{
|
||||
RenderWithColor(text, ConsoleColor.Magenta);
|
||||
RenderNewLine();
|
||||
}
|
||||
// Delimiter
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName != null)
|
||||
{
|
||||
Write('|');
|
||||
}
|
||||
|
||||
void RenderApplicationInfo()
|
||||
{
|
||||
if (!command.IsDefault)
|
||||
return;
|
||||
// Name
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Name))
|
||||
{
|
||||
Write(ConsoleColor.White, $"--{optionSchema.Name}");
|
||||
}
|
||||
|
||||
// Title and version
|
||||
RenderWithColor(_metadata.Title, ConsoleColor.Yellow);
|
||||
Render(" ");
|
||||
RenderWithColor(_metadata.VersionText, ConsoleColor.Yellow);
|
||||
RenderNewLine();
|
||||
WriteHorizontalColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(_metadata.Description))
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
|
||||
{
|
||||
Render(_metadata.Description);
|
||||
RenderNewLine();
|
||||
Write(optionSchema.Description);
|
||||
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))
|
||||
return;
|
||||
var relativeCommandName = !string.IsNullOrWhiteSpace(commandSchema.Name)
|
||||
? childCommandSchema.Name!.Substring(commandSchema.Name.Length + 1)
|
||||
: childCommandSchema.Name!;
|
||||
|
||||
RenderMargin();
|
||||
RenderHeader("Description");
|
||||
// Name
|
||||
WriteHorizontalMargin();
|
||||
Write(ConsoleColor.Cyan, relativeCommandName);
|
||||
|
||||
RenderIndent();
|
||||
Render(command.Description);
|
||||
RenderNewLine();
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
|
||||
{
|
||||
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();
|
||||
RenderHeader("Usage");
|
||||
|
||||
// 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();
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, commandSchema.Name);
|
||||
}
|
||||
|
||||
void RenderParameters()
|
||||
{
|
||||
if (!command.Parameters.Any())
|
||||
return;
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, "[command]");
|
||||
|
||||
RenderMargin();
|
||||
RenderHeader("Parameters");
|
||||
Write(' ');
|
||||
Write(ConsoleColor.White, "--help");
|
||||
|
||||
var parameters = command.Parameters
|
||||
.OrderBy(p => p.Order)
|
||||
.ToArray();
|
||||
Write("` to show help on a specific command.");
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
RenderWithColor("* ", ConsoleColor.Red);
|
||||
RenderWithColor($"{parameter.DisplayName}", ConsoleColor.White);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
RenderColumnIndent();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(parameter.Description))
|
||||
{
|
||||
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();
|
||||
}
|
||||
public void Write(ApplicationSchema applicationSchema, CommandSchema commandSchema)
|
||||
{
|
||||
var childCommandSchemas = applicationSchema.GetChildCommands(commandSchema.Name);
|
||||
var command = (ICommand) _typeActivator.CreateInstance(commandSchema.Type);
|
||||
|
||||
_console.ResetColor();
|
||||
RenderApplicationInfo();
|
||||
RenderDescription();
|
||||
RenderUsage();
|
||||
RenderParameters();
|
||||
RenderOptions();
|
||||
RenderChildCommands();
|
||||
|
||||
WriteApplicationInfo(commandSchema);
|
||||
WriteCommandDescription(commandSchema);
|
||||
WriteCommandUsage(commandSchema, childCommandSchemas);
|
||||
WriteCommandParameters(commandSchema);
|
||||
WriteCommandOptions(commandSchema, command);
|
||||
WriteCommandChildren(commandSchema, childCommandSchemas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
@@ -35,14 +36,6 @@ namespace CliFx.Exceptions
|
||||
/// </summary>
|
||||
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>
|
||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||
/// </summary>
|
||||
@@ -52,9 +45,18 @@ namespace CliFx.Exceptions
|
||||
ExitCode = exitCode != 0
|
||||
? exitCode
|
||||
: throw new ArgumentException("Exit code must not be zero in order to signify failure.");
|
||||
|
||||
HasMessage = !string.IsNullOrWhiteSpace(message);
|
||||
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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
internal static CliFxException DelegateActivatorReceivedNull(Type type)
|
||||
internal static CliFxException DelegateActivatorReturnedNull(Type type)
|
||||
{
|
||||
var message = $@"
|
||||
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());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsTooManyDefaults(
|
||||
IReadOnlyList<CommandSchema> invalidCommands)
|
||||
internal static CliFxException CommandsTooManyDefaults(IReadOnlyList<CommandSchema> invalidCommandSchemas)
|
||||
{
|
||||
var message = $@"
|
||||
Application configuration is invalid because there are {invalidCommands.Count} default commands:
|
||||
{string.Join(Environment.NewLine, invalidCommands.Select(p => p.Type.FullName))}
|
||||
Application configuration is invalid because there are {invalidCommandSchemas.Count} default commands:
|
||||
{invalidCommandSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
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.";
|
||||
@@ -127,11 +128,11 @@ Other commands must have unique non-empty names that identify them.";
|
||||
|
||||
internal static CliFxException CommandsDuplicateName(
|
||||
string name,
|
||||
IReadOnlyList<CommandSchema> invalidCommands)
|
||||
IReadOnlyList<CommandSchema> invalidCommandSchemas)
|
||||
{
|
||||
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))}
|
||||
Application configuration is invalid because there are {invalidCommandSchemas.Count} commands with the same name ('{name}'):
|
||||
{invalidCommandSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
Commands must have unique names.
|
||||
Names are not case-sensitive.";
|
||||
@@ -140,13 +141,13 @@ Names are not case-sensitive.";
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateOrder(
|
||||
CommandSchema command,
|
||||
CommandSchema commandSchema,
|
||||
int order,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
{
|
||||
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))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same order ({order}):
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
Parameters must have unique order.";
|
||||
|
||||
@@ -154,13 +155,13 @@ Parameters must have unique order.";
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateName(
|
||||
CommandSchema command,
|
||||
CommandSchema commandSchema,
|
||||
string name,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
{
|
||||
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))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same name ('{name}'):
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
Parameters must have unique names to avoid potential confusion in the help text.
|
||||
Names are not case-sensitive.";
|
||||
@@ -169,12 +170,12 @@ Names are not case-sensitive.";
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersTooManyNonScalar(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
{
|
||||
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))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} non-scalar parameters:
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
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.
|
||||
@@ -185,12 +186,12 @@ If it's not feasible to fit into these constraints, consider using options inste
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersNonLastNonScalar(
|
||||
CommandSchema command,
|
||||
CommandParameterSchema invalidParameter)
|
||||
CommandSchema commandSchema,
|
||||
CommandParameterSchema invalidParameterSchema)
|
||||
{
|
||||
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}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order:
|
||||
{invalidParameterSchema}
|
||||
|
||||
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.
|
||||
@@ -201,12 +202,12 @@ If it's not feasible to fit into these constraints, consider using options inste
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsNoName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains one or more options without a name:
|
||||
{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options without a name:
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
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(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
{
|
||||
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(o => $"{o.Property.Name} ('{o.DisplayName}')"))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options whose names are too short:
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
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.";
|
||||
@@ -228,30 +229,28 @@ If you intended to set the short name instead, use the attribute overload that a
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateName(
|
||||
CommandSchema command,
|
||||
CommandSchema commandSchema,
|
||||
string name,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
{
|
||||
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(o => o.Property.Name))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same name ('{name}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
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.";
|
||||
Options must have unique names.
|
||||
Names are not case-sensitive.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateShortName(
|
||||
CommandSchema command,
|
||||
CommandSchema commandSchema,
|
||||
char shortName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
{
|
||||
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(o => o.Property.Name))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same short name ('{shortName}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options must have unique 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(
|
||||
CommandSchema command,
|
||||
CommandSchema commandSchema,
|
||||
string environmentVariableName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
{
|
||||
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(o => o.Property.Name))}
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same fallback environment variable name ('{environmentVariableName}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options cannot share the same environment variable as a fallback.
|
||||
Environment variable names are not case-sensitive.";
|
||||
@@ -283,92 +282,148 @@ Environment variable names are not case-sensitive.";
|
||||
{
|
||||
var message = $@"
|
||||
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);
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
|
||||
CommandArgumentSchema argument,
|
||||
CommandParameterSchema parameterSchema,
|
||||
IReadOnlyList<string> values)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"Parameter <{argument.DisplayName}>"
|
||||
: $"Option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
{argumentDisplayText} expects a single value, but provided with multiple:
|
||||
{string.Join(", ", values.Select(v => $"'{v}'"))}";
|
||||
Parameter {parameterSchema.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(
|
||||
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(
|
||||
CommandArgumentSchema argument,
|
||||
CommandParameterSchema parameterSchema,
|
||||
string? value,
|
||||
Type type,
|
||||
Exception? innerException = null)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"parameter <{argument.DisplayName}>"
|
||||
: $"option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
Can't convert value '{value ?? "<null>"}' to type '{type.FullName}' for {argumentDisplayText}.
|
||||
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for parameter {parameterSchema.GetUserFacingDisplayString()}.
|
||||
{innerException?.Message ?? "This type is not supported."}";
|
||||
|
||||
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(
|
||||
CommandArgumentSchema argument,
|
||||
CommandParameterSchema parameterSchema,
|
||||
IReadOnlyList<string> values,
|
||||
Type type)
|
||||
{
|
||||
var argumentDisplayText = argument is CommandParameterSchema
|
||||
? $"parameter <{argument.DisplayName}>"
|
||||
: $"option '{argument.DisplayName}'";
|
||||
|
||||
var message = $@"
|
||||
Can't convert provided values to type '{type.FullName}' for {argumentDisplayText}:
|
||||
{string.Join(", ", values.Select(v => $"'{v}'"))}
|
||||
Can't convert provided values to type '{type.Name}' for parameter {parameterSchema.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);
|
||||
}
|
||||
|
||||
internal static CliFxException ParameterNotSet(CommandParameterSchema parameter)
|
||||
internal static CliFxException CannotConvertNonScalar(
|
||||
CommandOptionSchema optionSchema,
|
||||
IReadOnlyList<string> values,
|
||||
Type type)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 = $@"
|
||||
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);
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> inputs)
|
||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> argumentInputs)
|
||||
{
|
||||
var message = $@"
|
||||
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);
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> inputs)
|
||||
internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> optionInputs)
|
||||
{
|
||||
var message = $@"
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -14,16 +14,15 @@ namespace CliFx.Exceptions
|
||||
/// </summary>
|
||||
public CommandException(string? message, Exception? innerException,
|
||||
int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: base(message, innerException, exitCode, showHelp)
|
||||
: base(message, innerException, exitCode, showHelp)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandException"/>.
|
||||
/// </summary>
|
||||
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: this(message, null, exitCode, showHelp)
|
||||
: this(message, null, exitCode, showHelp)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace CliFx
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <remarks>If the execution of the command is not asynchronous, simply end the method with <code>return default;</code></remarks>
|
||||
ValueTask ExecuteAsync(IConsole console);
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace CliFx
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for a service can initialize objects at runtime.
|
||||
/// Abstraction for a service that can initialize objects at runtime.
|
||||
/// </summary>
|
||||
public interface ITypeActivator
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of specified type.
|
||||
/// Creates an instance of the specified type.
|
||||
/// </summary>
|
||||
object CreateInstance(Type type);
|
||||
}
|
||||
|
||||
13
CliFx/Internal/ProcessEx.cs
Normal file
13
CliFx/Internal/ProcessEx.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CliFx.Internal
|
||||
@@ -10,14 +10,17 @@ namespace CliFx.Internal
|
||||
|
||||
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) =>
|
||||
builder.Length > 0 ? builder.Append(value) : builder;
|
||||
|
||||
public static bool IsEmptyOrWhiteSpace(this string s) => s is object && string.IsNullOrWhiteSpace(s);
|
||||
|
||||
public static string WrapWithQuotesIfEmptyOrWhiteSpace(this string s) =>
|
||||
s.IsEmptyOrWhiteSpace() ? $"\"{s}\"" : s;
|
||||
|
||||
public static string ToCulturedString(this object obj, CultureInfo culture) => Convert.ToString(obj, culture);
|
||||
public static string ToFormattableString(this object obj,
|
||||
IFormatProvider? formatProvider = null, string? format = null) =>
|
||||
obj is IFormattable formattable
|
||||
? formattable.ToString(format, formatProvider)
|
||||
: obj.ToString();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CliFx.Internal
|
||||
{
|
||||
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 Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
|
||||
@@ -22,13 +28,30 @@ namespace CliFx.Internal
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
return type.GetGenericArguments().FirstOrDefault();
|
||||
|
||||
return type.GetInterfaces()
|
||||
return type
|
||||
.GetInterfaces()
|
||||
.Select(GetEnumerableUnderlyingType)
|
||||
.Where(t => t != null)
|
||||
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
||||
.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)
|
||||
{
|
||||
var sourceAsCollection = source as ICollection ?? source.ToArray();
|
||||
@@ -38,7 +61,5 @@ namespace CliFx.Internal
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static bool OverridesToStringMethod(this object obj) => obj?.ToString() != obj?.GetType().ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user