private object?ConvertMultiple( IMemberSchema memberSchema, IReadOnlyList <string> rawValues, Type targetEnumerableType, Type targetElementType) { var array = rawValues .Select(v => ConvertSingle(memberSchema, v, targetElementType)) .ToNonGenericArray(targetElementType); var arrayType = array.GetType(); // Assignable from an array (T[], IReadOnlyList<T>, etc) if (targetEnumerableType.IsAssignableFrom(arrayType)) { return(array); } // Array-constructible (List<T>, HashSet<T>, etc) var arrayConstructor = targetEnumerableType.GetConstructor(new[] { arrayType }); if (arrayConstructor is not null) { return(arrayConstructor.Invoke(new object?[] { array })); } throw CliFxException.InternalError( $"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type." + Environment.NewLine + $"There is no known way to convert an array of `{targetElementType.FullName}` into an instance of type `{targetEnumerableType.FullName}`." + Environment.NewLine + "To fix this, change the property to use a type which can be assigned from an array or a type that has a constructor which accepts an array." ); }
private void ValidateMember(IMemberSchema memberSchema, object?convertedValue) { var errors = new List <BindingValidationError>(); foreach (var validatorType in memberSchema.ValidatorTypes) { var validator = (IBindingValidator)_typeActivator.CreateInstance(validatorType); var error = validator.Validate(convertedValue); if (error is not null) { errors.Add(error); } } if (errors.Any()) { throw CliFxException.UserError( $"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value." + Environment.NewLine + "Error(s):" + Environment.NewLine + errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine) ); } }
/// <inheritdoc /> public object CreateInstance(Type type) { try { return(Activator.CreateInstance(type)); } catch (Exception ex) { throw CliFxException.DefaultActivatorFailed(type, ex); } }
private void BindParameters(ICommand instance, IReadOnlyList <CommandParameterInput> parameterInputs) { // All inputs must be bound var remainingParameterInputs = parameterInputs.ToList(); // Scalar parameters var scalarParameters = Parameters .OrderBy(p => p.Order) .TakeWhile(p => p.IsScalar) .ToArray(); for (var i = 0; i < scalarParameters.Length; i++) { var parameter = scalarParameters[i]; var scalarInput = i < parameterInputs.Count ? parameterInputs[i] : throw CliFxException.ParameterNotSet(parameter); parameter.BindOn(instance, scalarInput.Value); remainingParameterInputs.Remove(scalarInput); } // Non-scalar parameter (only one is allowed) var nonScalarParameter = Parameters .OrderBy(p => p.Order) .FirstOrDefault(p => !p.IsScalar); if (nonScalarParameter != null) { var nonScalarValues = parameterInputs .Skip(scalarParameters.Length) .Select(p => p.Value) .ToArray(); // Parameters are required by default and so a non-scalar parameter must // be bound to at least one value if (!nonScalarValues.Any()) { throw CliFxException.ParameterNotSet(nonScalarParameter); } nonScalarParameter.BindOn(instance, nonScalarValues); remainingParameterInputs.Clear(); } // Ensure all inputs were bound if (remainingParameterInputs.Any()) { throw CliFxException.UnrecognizedParametersProvided(remainingParameterInputs); } }
public ICommand InitializeEntryPoint( CommandLineInput commandLineInput, IReadOnlyDictionary <string, string> environmentVariables, ITypeActivator activator) { var command = TryFindCommand(commandLineInput, out var argumentOffset) ?? throw CliFxException.CannotFindMatchingCommand(commandLineInput); var parameterInputs = argumentOffset == 0 ? commandLineInput.UnboundArguments.ToArray() : commandLineInput.UnboundArguments.Skip(argumentOffset).ToArray(); return(command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator)); }
/// <inheritdoc /> public object CreateInstance(Type type) { try { return(Activator.CreateInstance(type)); } catch (Exception ex) { throw CliFxException.InternalError( $"Failed to create an instance of type `{type.FullName}`." + Environment.NewLine + "Default type activator is only capable of instantiating a type if it has a public parameterless constructor." + Environment.NewLine + "To fix this, either add a parameterless constructor to the type or configure a custom activator on the application.", ex ); } }
private object?ConvertMember(IMemberSchema memberSchema, IReadOnlyList <string> rawValues) { var targetType = memberSchema.Property.Type; try { // Non-scalar var enumerableUnderlyingType = targetType.TryGetEnumerableUnderlyingType(); if (targetType != typeof(string) && enumerableUnderlyingType is not null) { return(ConvertMultiple(memberSchema, rawValues, targetType, enumerableUnderlyingType)); } // Scalar if (rawValues.Count <= 1) { return(ConvertSingle(memberSchema, rawValues.SingleOrDefault(), targetType)); } } catch (Exception ex) when(ex is not CliFxException) // don't wrap CliFxException { // We use reflection-based invocation which can throw TargetInvocationException. // Unwrap these exceptions to provide a more user-friendly error message. var errorMessage = ex is TargetInvocationException invokeEx ? invokeEx.InnerException?.Message ?? invokeEx.Message : ex.Message; throw CliFxException.UserError( $"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" + Environment.NewLine + rawValues.Select(v => '<' + v + '>').JoinToString(" ") + Environment.NewLine + $"Error: {errorMessage}", ex ); } // Mismatch (scalar but too many values) throw CliFxException.UserError( $"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:" + Environment.NewLine + rawValues.Select(v => '<' + v + '>').JoinToString(" ") ); }
private void InjectParameters(ICommand command, IReadOnlyList <CommandUnboundArgumentInput> parameterInputs) { // All inputs must be bound var remainingParameterInputs = parameterInputs.ToList(); // Scalar parameters var scalarParameters = Parameters .OrderBy(p => p.Order) .TakeWhile(p => p.IsScalar) .ToArray(); for (var i = 0; i < scalarParameters.Length; i++) { var scalarParameter = scalarParameters[i]; var scalarParameterInput = i < parameterInputs.Count ? parameterInputs[i] : throw CliFxException.ParameterNotSet(scalarParameter); scalarParameter.Inject(command, scalarParameterInput.Value); remainingParameterInputs.Remove(scalarParameterInput); } // Non-scalar parameter (only one is allowed) var nonScalarParameter = Parameters .OrderBy(p => p.Order) .FirstOrDefault(p => !p.IsScalar); if (nonScalarParameter != null) { var nonScalarParameterValues = parameterInputs.Skip(scalarParameters.Length).Select(i => i.Value).ToArray(); nonScalarParameter.Inject(command, nonScalarParameterValues); remainingParameterInputs.Clear(); } // Ensure all inputs were bound if (remainingParameterInputs.Any()) { throw CliFxException.UnrecognizedParametersProvided(remainingParameterInputs); } }
private void BindOptions( ICommand instance, IReadOnlyList <CommandOptionInput> optionInputs, IReadOnlyDictionary <string, string> environmentVariables) { // All inputs must be bound var remainingOptionInputs = optionInputs.ToList(); // All required options must be set var unsetRequiredOptions = Options.Where(o => o.IsRequired).ToList(); // Environment variables foreach (var(name, value) in environmentVariables) { var option = Options.FirstOrDefault(o => o.MatchesEnvironmentVariableName(name)); if (option == null) { continue; } var values = option.IsScalar ? new[] { value } : value.Split(Path.PathSeparator); option.BindOn(instance, values); unsetRequiredOptions.Remove(option); } // Direct input foreach (var option in Options) { var inputs = optionInputs .Where(i => option.MatchesNameOrShortName(i.Alias)) .ToArray(); // Skip if the inputs weren't provided for this option if (!inputs.Any()) { continue; } var inputValues = inputs.SelectMany(i => i.Values).ToArray(); option.BindOn(instance, inputValues); remainingOptionInputs.RemoveRange(inputs); // Required option implies that the value has to be set and also be non-empty if (inputValues.Any()) { unsetRequiredOptions.Remove(option); } } // Ensure all inputs were bound if (remainingOptionInputs.Any()) { throw CliFxException.UnrecognizedOptionsProvided(remainingOptionInputs); } // Ensure all required options were set if (unsetRequiredOptions.Any()) { throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions); } }
private void BindOptions(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance) { // Ensure there are no unrecognized options and that all required options are set var remainingOptionInputs = commandInput.Options.ToList(); var remainingRequiredOptionSchemas = commandSchema.Options.Where(o => o.IsRequired).ToList(); foreach (var optionSchema in commandSchema.Options) { var optionInputs = commandInput .Options .Where(o => optionSchema.MatchesIdentifier(o.Identifier)) .ToArray(); var environmentVariableInput = commandInput .EnvironmentVariables .FirstOrDefault(e => optionSchema.MatchesEnvironmentVariable(e.Name)); // Direct input if (optionInputs.Any()) { var rawValues = optionInputs.SelectMany(o => o.Values).ToArray(); BindMember(optionSchema, commandInstance, rawValues); // Required options require at least one value to be set if (rawValues.Any()) { remainingRequiredOptionSchemas.Remove(optionSchema); } } // Environment variable else if (environmentVariableInput is not null) { var rawValues = optionSchema.Property.IsScalar() ? new[] { environmentVariableInput.Value } : environmentVariableInput.SplitValues(); BindMember(optionSchema, commandInstance, rawValues); // Required options require 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):" + Environment.NewLine + remainingOptionInputs .Select(o => o.GetFormattedIdentifier()) .JoinToString(", ") ); } if (remainingRequiredOptionSchemas.Any()) { throw CliFxException.UserError( "Missing required option(s):" + Environment.NewLine + remainingRequiredOptionSchemas .Select(o => o.GetFormattedIdentifier()) .JoinToString(", ") ); } }
private object?ConvertSingle(IMemberSchema memberSchema, string?rawValue, Type targetType) { // Custom converter if (memberSchema.ConverterType is not null) { var converter = (IBindingConverter)_typeActivator.CreateInstance(memberSchema.ConverterType); return(converter.Convert(rawValue)); } // Assignable from string (e.g. string itself, object, etc) if (targetType.IsAssignableFrom(typeof(string))) { return(rawValue); } // Special case for bool if (targetType == typeof(bool)) { return(string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue)); } // Special case for DateTimeOffset if (targetType == typeof(DateTimeOffset)) { return(DateTimeOffset.Parse(rawValue, _formatProvider)); } // Special case for TimeSpan if (targetType == typeof(TimeSpan)) { return(TimeSpan.Parse(rawValue, _formatProvider)); } // Enum if (targetType.IsEnum) { // Null reference exception will be handled upstream return(Enum.Parse(targetType, rawValue !, true)); } // Convertible primitives (int, double, char, etc) if (targetType.Implements(typeof(IConvertible))) { return(Convert.ChangeType(rawValue, targetType, _formatProvider)); } // Nullable<T> var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType(); if (nullableUnderlyingType is not null) { return(!string.IsNullOrWhiteSpace(rawValue) ? ConvertSingle(memberSchema, rawValue, nullableUnderlyingType) : null); } // String-constructible (FileInfo, etc) var stringConstructor = targetType.GetConstructor(new[] { typeof(string) }); if (stringConstructor is not null) { return(stringConstructor.Invoke(new object?[] { rawValue })); } // String-parseable (with IFormatProvider) var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true); if (parseMethodWithFormatProvider is not null) { return(parseMethodWithFormatProvider.Invoke(null, new object?[] { rawValue, _formatProvider })); } // String-parseable (without IFormatProvider) var parseMethod = targetType.TryGetStaticParseMethod(); if (parseMethod is not null) { return(parseMethod.Invoke(null, new object?[] { rawValue })); } throw CliFxException.InternalError( $"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type." + Environment.NewLine + $"There is no known way to convert a string value into an instance of type `{targetType.FullName}`." + Environment.NewLine + "To fix this, either change the property to use a supported type or configure a custom converter." ); }
private void BindParameters(CommandInput commandInput, CommandSchema commandSchema, ICommand commandInstance) { // Ensure there are no unexpected parameters and that all parameters are provided var remainingParameterInputs = commandInput.Parameters.ToList(); var remainingRequiredParameterSchemas = commandSchema.Parameters.Where(p => p.IsRequired).ToList(); var position = 0; foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order)) { // Break when there are no remaining inputs if (position >= commandInput.Parameters.Count) { break; } // Scalar - take one input at the current position if (parameterSchema.Property.IsScalar()) { var parameterInput = commandInput.Parameters[position]; var rawValues = new[] { parameterInput.Value }; BindMember(parameterSchema, commandInstance, rawValues); position++; remainingParameterInputs.Remove(parameterInput); } // Non-scalar - take all remaining inputs starting from the current position else { var parameterInputs = commandInput.Parameters.Skip(position).ToArray(); var rawValues = parameterInputs.Select(p => p.Value).ToArray(); BindMember(parameterSchema, commandInstance, rawValues); position += parameterInputs.Length; remainingParameterInputs.RemoveRange(parameterInputs); } remainingRequiredParameterSchemas.Remove(parameterSchema); } if (remainingParameterInputs.Any()) { throw CliFxException.UserError( "Unexpected parameter(s):" + Environment.NewLine + remainingParameterInputs .Select(p => p.GetFormattedIdentifier()) .JoinToString(" ") ); } if (remainingRequiredParameterSchemas.Any()) { throw CliFxException.UserError( "Missing required parameter(s):" + Environment.NewLine + remainingRequiredParameterSchemas .Select(o => o.GetFormattedIdentifier()) .JoinToString(" ") ); } }
private void InjectOptions( ICommand command, IReadOnlyList <CommandOptionInput> optionInputs, IReadOnlyDictionary <string, string> environmentVariables) { // All inputs must be bound var remainingOptionInputs = optionInputs.ToList(); // All required options must be set var unsetRequiredOptions = Options.Where(o => o.IsRequired).ToList(); // Environment variables foreach (var(name, value) in environmentVariables) { var option = Options.FirstOrDefault(o => o.MatchesEnvironmentVariableName(name)); if (option != null) { var values = option.IsScalar ? new[] { value } : value.Split(Path.PathSeparator); option.Inject(command, values); unsetRequiredOptions.Remove(option); } } // TODO: refactor this part? I wrote this while sick // Direct input foreach (var option in Options) { var inputs = optionInputs .Where(i => option.MatchesNameOrShortName(i.Alias)) .ToArray(); if (inputs.Any()) { var inputValues = inputs.SelectMany(i => i.Values).ToArray(); option.Inject(command, inputValues); foreach (var input in inputs) { remainingOptionInputs.Remove(input); } if (inputValues.Any()) { unsetRequiredOptions.Remove(option); } } } // Ensure all required options were set if (unsetRequiredOptions.Any()) { throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions); } // Ensure all inputs were bound if (remainingOptionInputs.Any()) { throw CliFxException.UnrecognizedOptionsProvided(remainingOptionInputs); } }
/// <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) { var showHelp = cfe.ShowHelp; var errorMessage = cfe.HasMessage ? cfe.Message : cfe.ToString(); _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage)); if (showHelp) { var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes); var commandLineInput = CommandLineInput.Parse(commandLineArguments); var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ?? CommandSchema.StubDefaultCommand; _helpTextWriter.Write(applicationSchema, commandSchema); } return(cfe.ExitCode); }