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.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
console.CursorLeft = 42;
console.CursorTop = 24;
// Assert
stdInData.Should().Be("input");

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -18,6 +18,6 @@ namespace CliFx
/// <inheritdoc />
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 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;
}
}
}

View File

@@ -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}]";
}

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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
);
}

View File

@@ -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))!;
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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)
{
}

View File

@@ -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);

View File

@@ -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);
}

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.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();
}
}

View File

@@ -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();
}
}