Esempio n. 1
0
    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."
                  );
    }
Esempio n. 2
0
    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)
                      );
        }
    }
Esempio n. 3
0
 /// <inheritdoc />
 public object CreateInstance(Type type)
 {
     try
     {
         return(Activator.CreateInstance(type));
     }
     catch (Exception ex)
     {
         throw CliFxException.DefaultActivatorFailed(type, ex);
     }
 }
Esempio n. 4
0
        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);
            }
        }
Esempio n. 5
0
        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));
        }
Esempio n. 6
0
 /// <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
                   );
     }
 }
Esempio n. 7
0
    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(" ")
                  );
    }
Esempio n. 8
0
        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);
            }
        }
Esempio n. 9
0
        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);
            }
        }
Esempio n. 10
0
    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(", ")
                      );
        }
    }
Esempio n. 11
0
    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."
                  );
    }
Esempio n. 12
0
    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(" ")
                      );
        }
    }
Esempio n. 13
0
        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);
            }
        }
Esempio n. 14
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)
        {
            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);
        }