mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-20 13:48:16 +08:00
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:

committed by
Patrik Svensson

parent
0bbf9b81a9
commit
0ae419326d
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
55
src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs
Normal file
55
src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
34
src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs
Normal file
34
src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs
Normal 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) ?? "?";
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
35
src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs
Normal file
35
src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
151
src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs
Normal file
151
src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
22
src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs
Normal file
22
src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs
Normal 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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user