mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
asd
This commit is contained in:
@@ -97,7 +97,7 @@ public class CliApplication(
|
||||
var commandInstance =
|
||||
commandSchema == FallbackDefaultCommand.Schema
|
||||
? new FallbackDefaultCommand() // bypass the activator
|
||||
: typeActivator.CreateInstance<IBindableCommand>(commandSchema.Type);
|
||||
: typeActivator.CreateInstance<ICommand>(commandSchema.Type);
|
||||
|
||||
// Assemble the help context
|
||||
var helpContext = new HelpContext(
|
||||
@@ -113,8 +113,8 @@ public class CliApplication(
|
||||
// propagate further.
|
||||
try
|
||||
{
|
||||
// Bind the command input to the command instance
|
||||
commandInstance.Bind(commandInput);
|
||||
// Activate the command instance with the provided input
|
||||
commandSchema.Activate(commandInput, commandInstance);
|
||||
|
||||
// Handle the version option
|
||||
if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true })
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using CliFx.Input;
|
||||
using CliFx.Schema;
|
||||
|
||||
namespace CliFx;
|
||||
@@ -11,8 +10,7 @@ namespace CliFx;
|
||||
// This command is only used as a stub for help text.
|
||||
[Command]
|
||||
internal partial class FallbackDefaultCommand
|
||||
: IBindableCommand,
|
||||
ICommandWithHelpOption,
|
||||
: ICommandWithHelpOption,
|
||||
ICommandWithVersionOption
|
||||
{
|
||||
[CommandHelpOption]
|
||||
@@ -21,11 +19,6 @@ internal partial class FallbackDefaultCommand
|
||||
[CommandVersionOption]
|
||||
public bool IsVersionRequested { get; init; }
|
||||
|
||||
public void Bind(CommandInput input)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
// Never actually executed
|
||||
[ExcludeFromCodeCoverage]
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
@@ -33,6 +26,5 @@ internal partial class FallbackDefaultCommand
|
||||
|
||||
internal partial class FallbackDefaultCommand
|
||||
{
|
||||
public static CommandSchema Schema { get; } =
|
||||
new(typeof(FallbackDefaultCommand), null, null, [], []);
|
||||
public static CommandSchema Schema { get; } = new CommandSchema<FallbackDefaultCommand>(null, null, []);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using CliFx.Input;
|
||||
|
||||
namespace CliFx;
|
||||
|
||||
/// <summary>
|
||||
/// Command whose inputs can be bound from command-line arguments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface is required to facilitate binding of command inputs (parameters and options)
|
||||
/// to their corresponding CLR properties.
|
||||
/// You should not need to implement this interface directly, as it will be automatically
|
||||
/// implemented by the framework.
|
||||
/// </remarks>
|
||||
public interface IBindableCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds the command input to the current instance.
|
||||
/// </summary>
|
||||
void Bind(CommandInput input);
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Input;
|
||||
using CliFx.Utils.Extensions;
|
||||
|
||||
namespace CliFx.Schema;
|
||||
|
||||
@@ -63,18 +66,160 @@ public class CommandSchema(
|
||||
|
||||
foreach (var parameterSchema in Parameters)
|
||||
{
|
||||
var value = parameterSchema.Property.Get(instance);
|
||||
var value = parameterSchema.Property.GetValue(instance);
|
||||
result[parameterSchema] = value;
|
||||
}
|
||||
|
||||
foreach (var optionSchema in Options)
|
||||
{
|
||||
var value = optionSchema.Property.Get(instance);
|
||||
var value = optionSchema.Property.GetValue(instance);
|
||||
result[optionSchema] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ActivateParameters(CommandInput input, ICommand instance)
|
||||
{
|
||||
// Ensure there are no unexpected parameters and that all parameters are provided
|
||||
var remainingParameterInputs = input.Parameters.ToList();
|
||||
var remainingRequiredParameterSchemas = Parameters.Where(p => p.IsRequired).ToList();
|
||||
|
||||
var position = 0;
|
||||
|
||||
foreach (var parameterSchema in Parameters.OrderBy(p => p.Order))
|
||||
{
|
||||
// Break when there are no remaining inputs
|
||||
if (position >= input.Parameters.Count)
|
||||
break;
|
||||
|
||||
// Non-sequence: take one input at the current position
|
||||
if (!parameterSchema.IsSequence)
|
||||
{
|
||||
var parameterInput = input.Parameters[position];
|
||||
parameterSchema.Activate(instance, [parameterInput.Value]);
|
||||
|
||||
position++;
|
||||
remainingParameterInputs.Remove(parameterInput);
|
||||
}
|
||||
// Sequence: take all remaining inputs starting from the current position
|
||||
else
|
||||
{
|
||||
var parameterInputs = input.Parameters.Skip(position).ToArray();
|
||||
|
||||
parameterSchema.Activate(
|
||||
instance,
|
||||
parameterInputs.Select(p => p.Value).ToArray()
|
||||
);
|
||||
|
||||
position += parameterInputs.Length;
|
||||
remainingParameterInputs.RemoveRange(parameterInputs);
|
||||
}
|
||||
|
||||
remainingRequiredParameterSchemas.Remove(parameterSchema);
|
||||
}
|
||||
|
||||
if (remainingParameterInputs.Any())
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
Unexpected parameter(s):
|
||||
{remainingParameterInputs.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")}
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
if (remainingRequiredParameterSchemas.Any())
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
Missing required parameter(s):
|
||||
{remainingRequiredParameterSchemas
|
||||
.Select(p => p.GetFormattedIdentifier())
|
||||
.JoinToString(" ")}
|
||||
"""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateOptions(CommandInput input, ICommand instance)
|
||||
{
|
||||
// Ensure there are no unrecognized options and that all required options are set
|
||||
var remainingOptionInputs = input.Options.ToList();
|
||||
var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired)
|
||||
.ToList();
|
||||
|
||||
foreach (var optionSchema in Options)
|
||||
{
|
||||
var optionInputs = input
|
||||
.Options.Where(o => optionSchema.MatchesIdentifier(o.Identifier))
|
||||
.ToArray();
|
||||
|
||||
var environmentVariableInput = input.EnvironmentVariables.FirstOrDefault(e =>
|
||||
optionSchema.MatchesEnvironmentVariable(e.Name)
|
||||
);
|
||||
|
||||
// Direct input
|
||||
if (optionInputs.Any())
|
||||
{
|
||||
var rawValues = optionInputs.SelectMany(o => o.Values).ToArray();
|
||||
|
||||
optionSchema.Activate(instance, rawValues);
|
||||
|
||||
// Required options need at least one value to be set
|
||||
if (rawValues.Any())
|
||||
remainingRequiredOptionSchemas.Remove(optionSchema);
|
||||
}
|
||||
// Environment variable
|
||||
else if (environmentVariableInput is not null)
|
||||
{
|
||||
var rawValues = !optionSchema.IsSequence
|
||||
? [environmentVariableInput.Value]
|
||||
: environmentVariableInput.SplitValues();
|
||||
|
||||
optionSchema.Activate(instance, rawValues);
|
||||
|
||||
// Required options need at least one value to be set
|
||||
if (rawValues.Any())
|
||||
remainingRequiredOptionSchemas.Remove(optionSchema);
|
||||
}
|
||||
// No input, skip
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
remainingOptionInputs.RemoveRange(optionInputs);
|
||||
}
|
||||
|
||||
if (remainingOptionInputs.Any())
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
Unrecognized option(s):
|
||||
{remainingOptionInputs.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")}
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
if (remainingRequiredOptionSchemas.Any())
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
Missing required option(s):
|
||||
{remainingRequiredOptionSchemas
|
||||
.Select(o => o.GetFormattedIdentifier())
|
||||
.JoinToString(", ")}
|
||||
"""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Activate(CommandInput input, ICommand instance)
|
||||
{
|
||||
ActivateParameters(input, instance);
|
||||
ActivateOptions(input, instance);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic version of the type is used to simplify initialization from the source-generated code
|
||||
|
||||
@@ -28,32 +28,31 @@ public abstract class InputSchema(
|
||||
public PropertyBinding Property { get; } = property;
|
||||
|
||||
/// <summary>
|
||||
/// Optional binding converter for this input.
|
||||
/// Binding converter used for this input.
|
||||
/// </summary>
|
||||
public IBindingConverter Converter { get; } = converter;
|
||||
|
||||
/// <summary>
|
||||
/// Optional binding validator(s) for this input.
|
||||
/// Binding validator(s) used for this input.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IBindingValidator> Validators { get; } = validators;
|
||||
|
||||
internal void Validate(object? value)
|
||||
{
|
||||
var errors = new List<BindingValidationError>();
|
||||
internal abstract string GetKind();
|
||||
|
||||
foreach (var validator in validators)
|
||||
{
|
||||
var error = validator.Validate(value);
|
||||
internal abstract string GetFormattedIdentifier();
|
||||
|
||||
if (error is not null)
|
||||
errors.Add(error);
|
||||
}
|
||||
private void Validate(object? value)
|
||||
{
|
||||
var errors = Validators
|
||||
.Select(validator => validator.Validate(value))
|
||||
.OfType<BindingValidationError>()
|
||||
.ToArray();
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value.
|
||||
{GetKind()} {GetFormattedIdentifier()} has been provided with an invalid value.
|
||||
Error(s):
|
||||
{errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)}
|
||||
"""
|
||||
@@ -61,17 +60,19 @@ public abstract class InputSchema(
|
||||
}
|
||||
}
|
||||
|
||||
internal void Set(ICommand command, IReadOnlyList<string?> rawInputs)
|
||||
internal void Activate(ICommand instance, IReadOnlyList<string?> rawInputs)
|
||||
{
|
||||
var formatProvider = CultureInfo.InvariantCulture;
|
||||
|
||||
try
|
||||
{
|
||||
// Multiple values expected, single or multiple values provided
|
||||
if (IsSequence)
|
||||
{
|
||||
var value = rawInputs.Select(v => Converter.Convert(v, formatProvider)).ToArray();
|
||||
Validate(value);
|
||||
|
||||
Property.Set(command, value);
|
||||
Property.SetValue(instance, value);
|
||||
}
|
||||
// Single value expected, single value provided
|
||||
else if (rawInputs.Count <= 1)
|
||||
@@ -79,19 +80,31 @@ public abstract class InputSchema(
|
||||
var value = Converter.Convert(rawInputs.SingleOrDefault(), formatProvider);
|
||||
Validate(value);
|
||||
|
||||
Property.Set(command, value);
|
||||
Property.SetValue(instance, value);
|
||||
}
|
||||
// Single value expected, multiple values provided
|
||||
else
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:
|
||||
{GetKind()} {GetFormattedIdentifier()} expects a single argument, but provided with multiple:
|
||||
{rawInputs.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||
"""
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
|
||||
{
|
||||
throw CliFxException.UserError(
|
||||
$"""
|
||||
{GetKind()} {GetFormattedIdentifier()} cannot be set from the provided argument(s):
|
||||
{rawInputs.Select(v => '<' + v + '>').JoinToString(" ")}
|
||||
Error: {ex.Message}
|
||||
""",
|
||||
ex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic version of the type is used to simplify initialization from the source-generated code
|
||||
|
||||
@@ -59,7 +59,9 @@ public class OptionSchema(
|
||||
!string.IsNullOrWhiteSpace(EnvironmentVariable)
|
||||
&& string.Equals(EnvironmentVariable, environmentVariableName, StringComparison.Ordinal);
|
||||
|
||||
internal string GetFormattedIdentifier()
|
||||
internal override string GetKind() => "Option";
|
||||
|
||||
internal override string GetFormattedIdentifier()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ public class ParameterSchema(
|
||||
/// </summary>
|
||||
public string? Description { get; } = description;
|
||||
|
||||
internal string GetFormattedIdentifier() => IsSequence ? $"<{Name}>" : $"<{Name}...>";
|
||||
internal override string GetKind() => "Parameter";
|
||||
|
||||
internal override string GetFormattedIdentifier() => IsSequence ? $"<{Name}>" : $"<{Name}...>";
|
||||
}
|
||||
|
||||
// Generic version of the type is used to simplify initialization from the source-generated code
|
||||
|
||||
@@ -13,8 +13,8 @@ public class PropertyBinding(
|
||||
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
Type type,
|
||||
Func<object, object?> get,
|
||||
Action<object, object?> set
|
||||
Func<object, object?> getValue,
|
||||
Action<object, object?> setValue
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -28,12 +28,12 @@ public class PropertyBinding(
|
||||
/// <summary>
|
||||
/// Gets the current value of the property on the specified instance.
|
||||
/// </summary>
|
||||
public object? Get(object instance) => get(instance);
|
||||
public object? GetValue(object instance) => getValue(instance);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current value of the property on the specified instance.
|
||||
/// </summary>
|
||||
public void Set(object instance, object? value) => set(instance, value);
|
||||
public void SetValue(object instance, object? value) => setValue(instance, value);
|
||||
|
||||
internal IReadOnlyList<object?>? TryGetValidValues()
|
||||
{
|
||||
@@ -65,9 +65,9 @@ public class PropertyBinding<
|
||||
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
TProperty
|
||||
>(Func<TObject, TProperty?> get, Action<TObject, TProperty?> set)
|
||||
>(Func<TObject, TProperty?> getValue, Action<TObject, TProperty?> setValue)
|
||||
: PropertyBinding(
|
||||
typeof(TProperty),
|
||||
o => get((TObject)o),
|
||||
(o, v) => set((TObject)o, (TProperty?)v)
|
||||
o => getValue((TObject)o),
|
||||
(o, v) => setValue((TObject)o, (TProperty?)v)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -14,16 +13,6 @@ internal static class CollectionExtensions
|
||||
yield return (o, i++);
|
||||
}
|
||||
|
||||
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
|
||||
where T : class
|
||||
{
|
||||
foreach (var i in source)
|
||||
{
|
||||
if (i is not null)
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> WhereNotNullOrWhiteSpace(this IEnumerable<string?> source)
|
||||
{
|
||||
foreach (var i in source)
|
||||
@@ -47,14 +36,4 @@ internal static class CollectionExtensions
|
||||
dictionary
|
||||
.Cast<DictionaryEntry>()
|
||||
.ToDictionary(entry => (TKey)entry.Key, entry => (TValue)entry.Value!, comparer);
|
||||
|
||||
public static Array ToNonGenericArray<T>(this IEnumerable<T> source, Type elementType)
|
||||
{
|
||||
var sourceAsCollection = source as ICollection ?? source.ToArray();
|
||||
|
||||
var array = Array.CreateInstance(elementType, sourceAsCollection.Count);
|
||||
sourceAsCollection.CopyTo(array, 0);
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,11 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CliFx.Utils.Extensions;
|
||||
|
||||
internal static class TypeExtensions
|
||||
{
|
||||
public static bool Implements(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type,
|
||||
Type interfaceType
|
||||
) => type.GetInterfaces().Contains(interfaceType);
|
||||
|
||||
public static Type? TryGetNullableUnderlyingType(this Type type) =>
|
||||
Nullable.GetUnderlyingType(type);
|
||||
|
||||
public static Type? TryGetEnumerableUnderlyingType(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type
|
||||
)
|
||||
@@ -39,23 +30,11 @@ internal static class TypeExtensions
|
||||
.MaxBy(t => t != typeof(object));
|
||||
}
|
||||
|
||||
public static MethodInfo? TryGetStaticParseMethod(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type,
|
||||
bool withFormatProvider = false
|
||||
) =>
|
||||
type.GetMethod(
|
||||
"Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
withFormatProvider ? [typeof(string), typeof(IFormatProvider)] : [typeof(string)],
|
||||
null
|
||||
);
|
||||
|
||||
public static bool IsToStringOverriden(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type
|
||||
)
|
||||
{
|
||||
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);
|
||||
return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType;
|
||||
return toStringMethod?.GetBaseDefinition().DeclaringType != toStringMethod?.DeclaringType;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user