Add Spectre.Cli to Spectre.Console

* Renames Spectre.Cli to Spectre.Console.Cli.
* Now uses Verify with Spectre.Console.Cli tests.
* Removes some duplicate definitions.

Closes #168
This commit is contained in:
Patrik Svensson
2020-12-23 10:41:29 +01:00
committed by Patrik Svensson
parent 0bbf9b81a9
commit 0ae419326d
361 changed files with 13934 additions and 604 deletions

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal sealed class CommandArgument : CommandParameter
{
public string Value { get; }
public int Position { get; set; }
public CommandArgument(
Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description,
TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue,
CommandArgumentAttribute argument, IEnumerable<ParameterValidationAttribute> validators)
: base(parameterType, parameterKind, property, description, converter, defaultValue,
null, validators, argument.IsRequired)
{
Value = argument.ValueName;
Position = argument.Position;
}
}
}

View File

@ -0,0 +1,21 @@
using System.Linq;
namespace Spectre.Console.Cli.Internal
{
internal static class CommandContainerExtensions
{
public static CommandInfo? FindCommand(this ICommandContainer root, string name, CaseSensitivity sensitivity)
{
var result = root.Commands.FirstOrDefault(
c => c.Name.Equals(name, sensitivity.GetStringComparison(CommandPart.CommandName)));
if (result == null)
{
result = root.Commands.FirstOrDefault(
c => c.Aliases.Contains(name, sensitivity.GetStringComparer(CommandPart.CommandName)));
}
return result;
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal sealed class CommandInfo : ICommandContainer
{
public string Name { get; }
public HashSet<string> Aliases { get; }
public string? Description { get; }
public object? Data { get; }
public Type? CommandType { get; }
public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, int>? Delegate { get; }
public bool IsDefaultCommand { get; }
public bool IsHidden { get; }
public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; }
public IList<CommandParameter> Parameters { get; }
public IList<string[]> Examples { get; }
public bool IsBranch => CommandType == null && Delegate == null;
IList<CommandInfo> ICommandContainer.Commands => Children;
public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{
Parent = parent;
Name = prototype.Name;
Aliases = new HashSet<string>(prototype.Aliases);
Description = prototype.Description;
Data = prototype.Data;
CommandType = prototype.CommandType;
SettingsType = prototype.SettingsType;
Delegate = prototype.Delegate;
IsDefaultCommand = prototype.IsDefaultCommand;
IsHidden = prototype.IsHidden;
Children = new List<CommandInfo>();
Parameters = new List<CommandParameter>();
Examples = prototype.Examples ?? new List<string[]>();
if (CommandType != null && string.IsNullOrWhiteSpace(Description))
{
var description = CommandType.GetCustomAttribute<DescriptionAttribute>();
if (description != null)
{
Description = description.Description;
}
}
}
}
}

View File

@ -0,0 +1,79 @@
using System.Linq;
namespace Spectre.Console.Cli.Internal
{
internal static class CommandInfoExtensions
{
public static bool HaveParentWithOption(this CommandInfo command, CommandOption option)
{
var parent = command?.Parent;
while (parent != null)
{
foreach (var parentOption in parent.Parameters.OfType<CommandOption>())
{
if (option.HaveSameBackingPropertyAs(parentOption))
{
return true;
}
}
parent = parent.Parent;
}
return false;
}
public static bool AllowParentOption(this CommandInfo command, CommandOption option)
{
// Got an immediate parent?
if (command?.Parent != null)
{
// Is the current node's settings type the same as the previous one?
if (command.SettingsType == command.Parent.SettingsType)
{
var parameters = command.Parent.Parameters.OfType<CommandOption>().ToArray();
if (parameters.Length > 0)
{
foreach (var parentOption in parameters)
{
// Is this the same one?
if (option.HaveSameBackingPropertyAs(parentOption))
{
// Is it part of the same settings class.
if (option.Property.DeclaringType == command.SettingsType)
{
// Allow it.
return true;
}
// Don't allow it.
return false;
}
}
}
}
}
return false;
}
public static bool HaveParentWithArgument(this CommandInfo command, CommandArgument argument)
{
var parent = command?.Parent;
while (parent != null)
{
foreach (var parentOption in parent.Parameters.OfType<CommandArgument>())
{
if (argument.HaveSameBackingPropertyAs(parentOption))
{
return true;
}
}
parent = parent.Parent;
}
return false;
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal sealed class CommandModel : ICommandContainer
{
public string? ApplicationName { get; }
public ParsingMode ParsingMode { get; }
public CommandInfo? DefaultCommand { get; }
public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; }
public CommandModel(
CommandAppSettings settings,
CommandInfo? defaultCommand,
IEnumerable<CommandInfo> commands,
IEnumerable<string[]> examples)
{
ApplicationName = settings.ApplicationName;
ParsingMode = settings.ParsingMode;
DefaultCommand = defaultCommand;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
}
public string GetApplicationName()
{
return ApplicationName ?? Path.GetFileName(Assembly.GetEntryAssembly()?.Location) ?? "?";
}
}
}

View File

@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal static class CommandModelBuilder
{
// Consider removing this in favor for value tuples at some point.
private sealed class OrderedProperties
{
public int Level { get; }
public int SortOrder { get; }
public PropertyInfo[] Properties { get; }
public OrderedProperties(int level, int sortOrder, PropertyInfo[] properties)
{
Level = level;
SortOrder = sortOrder;
Properties = properties;
}
}
public static CommandModel Build(IConfiguration configuration)
{
var result = new List<CommandInfo>();
foreach (var command in configuration.Commands)
{
result.Add(Build(null, command));
}
var defaultCommand = default(CommandInfo);
if (configuration.DefaultCommand != null)
{
// Add the examples from the configuration to the default command.
configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// Build the default command.
defaultCommand = Build(null, configuration.DefaultCommand);
}
// Create the command model and validate it.
var model = new CommandModel(configuration.Settings, defaultCommand, result, configuration.Examples);
CommandModelValidator.Validate(model, configuration.Settings);
return model;
}
private static CommandInfo Build(CommandInfo? parent, ConfiguredCommand command)
{
var info = new CommandInfo(parent, command);
foreach (var parameter in GetParameters(info))
{
info.Parameters.Add(parameter);
}
foreach (var childCommand in command.Children)
{
var child = Build(info, childCommand);
info.Children.Add(child);
}
// Normalize argument positions.
var index = 0;
foreach (var argument in info.Parameters.OfType<CommandArgument>()
.OrderBy(argument => argument.Position))
{
argument.Position = index;
index++;
}
return info;
}
private static IEnumerable<CommandParameter> GetParameters(CommandInfo command)
{
var result = new List<CommandParameter>();
var argumentPosition = 0;
// We need to get parameters in order of the class where they were defined.
// We assign each inheritance level a value that is used to properly sort the
// arguments when iterating over them.
IEnumerable<OrderedProperties> GetPropertiesInOrder()
{
var current = command.SettingsType;
var level = 0;
var sortOrder = 0;
while (current.BaseType != null)
{
yield return new OrderedProperties(level, sortOrder, current.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public));
current = current.BaseType;
// Things get a little bit complicated now.
// Only consider a setting's base type part of the
// setting, if there isn't a parent command that implements
// the setting's base type. This might come back to bite us :)
var currentCommand = command.Parent;
while (currentCommand != null)
{
if (currentCommand.SettingsType == current)
{
level--;
break;
}
currentCommand = currentCommand.Parent;
}
sortOrder--;
}
}
var groups = GetPropertiesInOrder();
foreach (var group in groups.OrderBy(x => x.Level).ThenBy(x => x.SortOrder))
{
var parameters = new List<CommandParameter>();
foreach (var property in group.Properties)
{
if (property.IsDefined(typeof(CommandOptionAttribute)))
{
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
if (attribute != null)
{
var option = BuildOptionParameter(property, attribute);
// Any previous command has this option defined?
if (command.HaveParentWithOption(option))
{
// Do we allow it to exist on this command as well?
if (command.AllowParentOption(option))
{
option.IsShadowed = true;
parameters.Add(option);
}
}
else
{
// No parent have this option.
parameters.Add(option);
}
}
}
else if (property.IsDefined(typeof(CommandArgumentAttribute)))
{
var attribute = property.GetCustomAttribute<CommandArgumentAttribute>();
if (attribute != null)
{
var argument = BuildArgumentParameter(property, attribute);
// Any previous command has this argument defined?
// In that case, we should not assign the parameter to this command.
if (!command.HaveParentWithArgument(argument))
{
parameters.Add(argument);
}
}
}
}
// Update the position for the parameters.
foreach (var argument in parameters.OfType<CommandArgument>().OrderBy(x => x.Position))
{
argument.Position = argumentPosition++;
}
// Add all parameters to the result.
foreach (var groupResult in parameters)
{
result.Add(groupResult);
}
}
return result;
}
private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var kind = GetOptionKind(property.PropertyType, attribute, deconstructor, converter);
if (defaultValue == null && property.PropertyType == typeof(bool))
{
defaultValue = new DefaultValueAttribute(false);
}
return new CommandOption(property.PropertyType, kind,
property, description?.Description, converter, deconstructor,
attribute, validators, defaultValue, attribute.ValueIsOptional);
}
private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var kind = GetParameterKind(property.PropertyType);
return new CommandArgument(
property.PropertyType, kind, property,
description?.Description, converter,
defaultValue, attribute, validators);
}
private static ParameterKind GetOptionKind(
Type type,
CommandOptionAttribute attribute,
PairDeconstructorAttribute? deconstructor,
TypeConverterAttribute? converter)
{
if (attribute.ValueIsOptional)
{
return ParameterKind.FlagWithValue;
}
if (type.IsPairDeconstructable() && (deconstructor != null || converter == null))
{
return ParameterKind.Pair;
}
return GetParameterKind(type);
}
private static ParameterKind GetParameterKind(Type type)
{
if (type == typeof(bool))
{
return ParameterKind.Flag;
}
if (type.IsArray)
{
return ParameterKind.Vector;
}
return ParameterKind.Scalar;
}
}
}

View File

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console.Cli.Internal
{
internal static class CommandModelValidator
{
public static void Validate(CommandModel model, CommandAppSettings settings)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
if (model.Commands.Count == 0 && model.DefaultCommand == null)
{
throw CommandConfigurationException.NoCommandConfigured();
}
foreach (var command in model.Commands)
{
// Alias collision?
foreach (var alias in command.Aliases)
{
if (model.Commands.Any(x => x.Name.Equals(alias, StringComparison.OrdinalIgnoreCase)))
{
throw CommandConfigurationException.CommandNameConflict(command, alias);
}
}
}
Validate(model.DefaultCommand);
foreach (var command in model.Commands)
{
Validate(command);
}
if (settings.ValidateExamples)
{
ValidateExamples(model, settings);
}
}
private static void Validate(CommandInfo? command)
{
if (command == null)
{
return;
}
// Get duplicate options for command.
var duplicateOptions = GetDuplicates(command);
if (duplicateOptions.Length > 0)
{
throw CommandConfigurationException.DuplicateOption(command, duplicateOptions);
}
// No children?
if (command.IsBranch && command.Children.Count == 0)
{
throw CommandConfigurationException.BranchHasNoChildren(command);
}
// Multiple vector arguments?
var arguments = command.Parameters.OfType<CommandArgument>();
if (arguments.Any(x => x.ParameterKind == ParameterKind.Vector))
{
// Multiple vector arguments for command?
if (arguments.Count(x => x.ParameterKind == ParameterKind.Vector) > 1)
{
throw CommandConfigurationException.TooManyVectorArguments(command);
}
// Make sure that vector arguments are specified last.
if (arguments.Last().ParameterKind != ParameterKind.Vector)
{
throw CommandConfigurationException.VectorArgumentNotSpecifiedLast(command);
}
}
// Arguments
var argumnets = command.Parameters.OfType<CommandArgument>();
foreach (var argument in arguments)
{
if (argument.Required && argument.DefaultValue != null)
{
throw CommandConfigurationException.RequiredArgumentsCannotHaveDefaultValue(argument);
}
}
// Options
var options = command.Parameters.OfType<CommandOption>();
foreach (var option in options)
{
// Pair deconstructable?
if (option.Property.PropertyType.IsPairDeconstructable())
{
if (option.PairDeconstructor != null && option.Converter != null)
{
throw CommandConfigurationException.OptionBothHasPairDeconstructorAndTypeParameter(option);
}
}
else if (option.PairDeconstructor != null)
{
throw CommandConfigurationException.OptionTypeDoesNotSupportDeconstruction(option);
}
// Optional options that are not flags?
if (option.ParameterKind == ParameterKind.FlagWithValue && !option.IsFlagValue())
{
throw CommandConfigurationException.OptionalOptionValueMustBeFlagWithValue(option);
}
}
// Validate child commands.
foreach (var childCommand in command.Children)
{
Validate(childCommand);
}
}
private static void ValidateExamples(CommandModel model, CommandAppSettings settings)
{
var examples = new List<string[]>();
examples.AddRangeIfNotNull(model.Examples);
// Get all examples.
var queue = new Queue<ICommandContainer>(new[] { model });
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var command in current.Commands)
{
examples.AddRangeIfNotNull(command.Examples);
queue.Enqueue(command);
}
}
// Validate all examples.
foreach (var example in examples)
{
try
{
var parser = new CommandTreeParser(model, settings, ParsingMode.Strict);
parser.Parse(example);
}
catch (Exception ex)
{
throw new CommandConfigurationException("Validation of examples failed.", ex);
}
}
}
private static string[] GetDuplicates(CommandInfo command)
{
var result = new Dictionary<string, int>(StringComparer.Ordinal);
void AddToResult(IEnumerable<string> keys)
{
foreach (var key in keys)
{
if (!string.IsNullOrWhiteSpace(key))
{
if (!result.ContainsKey(key))
{
result.Add(key, 0);
}
result[key]++;
}
}
}
foreach (var option in command.Parameters.OfType<CommandOption>())
{
AddToResult(option.ShortNames);
AddToResult(option.LongNames);
}
return result.Where(x => x.Value > 1)
.Select(x => x.Key).ToArray();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal sealed class CommandOption : CommandParameter
{
public IReadOnlyList<string> LongNames { get; }
public IReadOnlyList<string> ShortNames { get; }
public string? ValueName { get; }
public bool ValueIsOptional { get; }
public bool IsShadowed { get; set; }
public CommandOption(
Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description,
TypeConverterAttribute? converter, PairDeconstructorAttribute? deconstructor,
CommandOptionAttribute optionAttribute, IEnumerable<ParameterValidationAttribute> validators,
DefaultValueAttribute? defaultValue, bool valueIsOptional)
: base(parameterType, parameterKind, property, description, converter,
defaultValue, deconstructor, validators, false)
{
LongNames = optionAttribute.LongNames;
ShortNames = optionAttribute.ShortNames;
ValueName = optionAttribute.ValueName;
ValueIsOptional = valueIsOptional;
}
public string GetOptionName()
{
return LongNames.Count > 0 ? LongNames[0] : ShortNames[0];
}
}
}

View File

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace Spectre.Console.Cli.Internal
{
internal abstract class CommandParameter : ICommandParameterInfo
{
public Guid Id { get; }
public Type ParameterType { get; }
public ParameterKind ParameterKind { get; }
public PropertyInfo Property { get; }
public string? Description { get; }
public DefaultValueAttribute? DefaultValue { get; }
public TypeConverterAttribute? Converter { get; }
public PairDeconstructorAttribute? PairDeconstructor { get; }
public List<ParameterValidationAttribute> Validators { get; }
public bool Required { get; set; }
public string PropertyName => Property.Name;
public virtual bool WantRawValue => ParameterType.IsPairDeconstructable()
&& (PairDeconstructor != null || Converter == null);
protected CommandParameter(
Type parameterType, ParameterKind parameterKind, PropertyInfo property,
string? description, TypeConverterAttribute? converter,
DefaultValueAttribute? defaultValue,
PairDeconstructorAttribute? deconstuctor,
IEnumerable<ParameterValidationAttribute> validators, bool required)
{
Id = Guid.NewGuid();
ParameterType = parameterType;
ParameterKind = parameterKind;
Property = property;
Description = description;
Converter = converter;
DefaultValue = defaultValue;
PairDeconstructor = deconstuctor;
Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>());
Required = required;
}
public bool IsFlagValue()
{
return ParameterType.GetInterfaces().Any(i => i == typeof(IFlagValue));
}
public bool HaveSameBackingPropertyAs(CommandParameter other)
{
return CommandParameterComparer.ByBackingProperty.Equals(this, other);
}
public void Assign(CommandSettings settings, ITypeResolver resolver, object? value)
{
// Is the property pair deconstructable?
// TODO: This needs to be better defined
if (Property.PropertyType.IsPairDeconstructable() && WantRawValue)
{
var genericTypes = Property.PropertyType.GetGenericArguments();
var multimap = (IMultiMap?)Property.GetValue(settings);
if (multimap == null)
{
multimap = Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(genericTypes[0], genericTypes[1])) as IMultiMap;
if (multimap == null)
{
throw new InvalidOperationException("Could not create multimap");
}
}
// Create deconstructor.
var deconstructorType = PairDeconstructor?.Type ?? typeof(DefaultPairDeconstructor);
if (!(resolver.Resolve(deconstructorType) is IPairDeconstructor deconstructor))
{
if (!(Activator.CreateInstance(deconstructorType) is IPairDeconstructor activatedDeconstructor))
{
throw new InvalidOperationException($"Could not create pair deconstructor.");
}
deconstructor = activatedDeconstructor;
}
// Deconstruct and add to multimap.
var pair = deconstructor.Deconstruct(resolver, genericTypes[0], genericTypes[1], value as string);
if (pair.Key != null)
{
multimap.Add(pair);
}
value = multimap;
}
else if (Property.PropertyType.IsArray)
{
// Add a new item to the array
var array = (Array?)Property.GetValue(settings);
Array newArray;
var elementType = Property.PropertyType.GetElementType();
if (elementType == null)
{
throw new InvalidOperationException("Could not get property type.");
}
if (array == null)
{
newArray = Array.CreateInstance(elementType, 1);
}
else
{
newArray = Array.CreateInstance(elementType, array.Length + 1);
array.CopyTo(newArray, 0);
}
newArray.SetValue(value, newArray.Length - 1);
value = newArray;
}
else if (IsFlagValue())
{
var flagValue = (IFlagValue?)Property.GetValue(settings);
if (flagValue == null)
{
flagValue = (IFlagValue?)Activator.CreateInstance(ParameterType);
if (flagValue == null)
{
throw new InvalidOperationException("Could not create flag value.");
}
}
if (value != null)
{
// Null means set, but not with a valid value.
flagValue.Value = value;
}
// If the parameter was mapped, then it's set.
flagValue.IsSet = true;
value = flagValue;
}
Property.SetValue(settings, value);
}
public object? Get(CommandSettings settings)
{
return Property.GetValue(settings);
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace Spectre.Console.Cli.Internal
{
internal static class CommandParameterComparer
{
public static readonly ByBackingPropertyComparer ByBackingProperty = new ByBackingPropertyComparer();
public sealed class ByBackingPropertyComparer : IEqualityComparer<CommandParameter?>
{
public bool Equals(CommandParameter? x, CommandParameter? y)
{
if (x is null || y is null)
{
return false;
}
if (ReferenceEquals(x, y))
{
return true;
}
return x.Property.MetadataToken == y.Property.MetadataToken;
}
public int GetHashCode(CommandParameter? obj)
{
return obj?.Property?.MetadataToken.GetHashCode() ?? 0;
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Spectre.Console.Cli.Internal
{
/// <summary>
/// Represents a command container.
/// </summary>
internal interface ICommandContainer
{
/// <summary>
/// Gets all commands in the container.
/// </summary>
IList<CommandInfo> Commands { get; }
}
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel;
namespace Spectre.Console.Cli.Internal
{
internal enum ParameterKind
{
[Description("flag")]
Flag = 0,
[Description("scalar")]
Scalar = 1,
[Description("vector")]
Vector = 2,
[Description("flagvalue")]
FlagWithValue = 3,
[Description("pair")]
Pair = 4,
}
}