Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/CommandLineUtils/Abstractions/IValueParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace McMaster.Extensions.CommandLineUtils.Abstractions
{
using System;

/// <summary>
/// The interface for defining a value parser.
/// </summary>
public interface IValueParser
{
/// <summary>
/// Gets the Type that this value parser is defined for.
/// </summary>
Type TargetType { get; }

/// <summary>
/// Parses the raw string value.
/// </summary>
/// <param name="argName">The name of the argument this value will be bound to.</param>
/// <param name="value">The raw string value to parse.</param>
/// <returns>The parsed value object.</returns>
/// <throws name="System.FormatException">When the value cannot be parsed.</throws>
object Parse(string argName, string value);
}
}
149 changes: 149 additions & 0 deletions src/CommandLineUtils/Abstractions/ValueParserProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace McMaster.Extensions.CommandLineUtils.Abstractions
{
using System;
using System.Collections.Generic;
using System.Reflection;

/// <summary>
/// A store of value parsers that are used to convert argument values from strings to types.
/// </summary>
public class ValueParserProvider
{
private readonly Dictionary<Type, IValueParser> _parsers = new Dictionary<Type, IValueParser>(10);

internal ValueParserProvider()
{
this.AddRange(
new IValueParser[]
{
StringValueParser.Singleton,
BooleanValueParser.Singleton,
ByteValueParser.Singleton,
Int16ValueParser.Singleton,
Int32ValueParser.Singleton,
Int64ValueParser.Singleton,
UInt16ValueParser.Singleton,
UInt32ValueParser.Singleton,
UInt64ValueParser.Singleton,
FloatValueParser.Singleton,
DoubleValueParser.Singleton,
});
}

internal IValueParser GetParser(Type type)
{
if (this._parsers.TryGetValue(type, out var parser))
{
return parser;
}

var typeInfo = type.GetTypeInfo();

if (typeInfo.IsEnum)
{
return new EnumParser(type);
}

if (ReflectionHelper.IsNullableType(typeInfo, out var wrappedType))
{
if (wrappedType.GetTypeInfo().IsEnum)
{
return new NullableValueParser(new EnumParser(wrappedType));
}

if (this._parsers.TryGetValue(wrappedType, out parser))
{
return new NullableValueParser(parser);
}
}

return parser;
}


/// <summary>
/// Add a new value parser to the provider.
/// </summary>
/// <param name="parser">An instance of the parser that is used to convert an argument from a string.</param>
/// <exception cref="ArgumentException">
/// A value parser with the same <see cref="IValueParser.TargetType"/> is already registered.
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="parser"/> is null.</exception>
public void Add(IValueParser parser)
{
this.SafeAdd(parser);
}

/// <summary>
/// Add collection of a new value parsers to the provider.
/// </summary>
/// <param name="parsers">The collection whose parsers should be added.</param>
/// <exception cref="ArgumentException">
/// A value parser with the same <see cref="IValueParser.TargetType"/> is already registered.
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="parsers"/> is null.</exception>
public void AddRange(IEnumerable<IValueParser> parsers)
{
if (parsers == null)
{
throw new ArgumentNullException(nameof(parsers));
}

foreach (var parser in parsers)
{
this.SafeAdd(parser);
}
}

/// <summary>
/// Add a new value parser to the provider, or if a value provider already exists for
/// <see cref="IValueParser.TargetType"/> then replaces it with <paramref name="parser"/>.
/// </summary>
/// <param name="parser">An instance of the parser that is used to convert an argument from a string.</param>
/// <exception cref="ArgumentNullException"><paramref name="parser"/> is null.</exception>
public void AddOrReplace(IValueParser parser)
{
this.SafeAdd(parser, andReplace: true);
}

private void SafeAdd(IValueParser parser, bool andReplace = false)
{
if (parser == null)
{
throw new ArgumentNullException(nameof(parser));
}

var targetType = parser.TargetType;

if (targetType == null)
{
throw new ArgumentNullException(
nameof(IValueParser.TargetType),
"The value parser must have a target type set");
}

// strip nullable wrappers since we have a dedicated nullable value parser
targetType = ReflectionHelper.IsNullableType(targetType.GetTypeInfo(), out var wrappedType)
? wrappedType
: targetType;

if (this._parsers.ContainsKey(targetType))
{
if (andReplace)
{
this._parsers.Remove(targetType);
}
else
{
throw new ArgumentException(
$"Value parser provider for type '{targetType}' already exists.");
}
}

this._parsers.Add(targetType, parser);
}
}
}
7 changes: 4 additions & 3 deletions src/CommandLineUtils/Attributes/OptionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Reflection;
using McMaster.Extensions.CommandLineUtils.Abstractions;

namespace McMaster.Extensions.CommandLineUtils
{
Expand Down Expand Up @@ -66,7 +67,7 @@ public OptionAttribute(string template, string description, CommandOptionType op

internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)
{
var optionType = GetOptionType(prop);
var optionType = GetOptionType(prop, app.ValueParsers);
CommandOption option;
if (Template != null)
{
Expand Down Expand Up @@ -94,14 +95,14 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)
return option;
}

private CommandOptionType GetOptionType(PropertyInfo prop)
private CommandOptionType GetOptionType(PropertyInfo prop, ValueParserProvider valueParsers)
{
CommandOptionType optionType;
if (OptionType.HasValue)
{
optionType = OptionType.Value;
}
else if (!CommandOptionTypeMapper.Default.TryGetOptionType(prop.PropertyType, out optionType))
else if (!CommandOptionTypeMapper.Default.TryGetOptionType(prop.PropertyType, valueParsers, out optionType))
{
throw new InvalidOperationException(Strings.CannotDetermineOptionType(prop));
}
Expand Down
2 changes: 1 addition & 1 deletion src/CommandLineUtils/CommandLineApplication.Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static int Execute<TApp>(CommandLineContext context)
return app.Execute(context.Arguments);
}
}
catch (CommandParsingException ex)
catch (Exception ex) when (ex is CommandParsingException || ex is FormatException)
{
context.Console.Error.WriteLine(ex.Message);
return ValidationErrorExitCode;
Expand Down
14 changes: 14 additions & 0 deletions src/CommandLineUtils/CommandLineApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ internal CommandLineApplication(CommandLineApplication parent,
ValidationErrorHandler = DefaultValidationErrorHandler;
SetContext(context);
_services = new Lazy<IServiceProvider>(() => new ServiceProvider(this));
ValueParsers = new ValueParserProvider();

_conventionContext = CreateConventionContext();

Expand Down Expand Up @@ -247,6 +248,19 @@ public CommandOption OptionHelp
/// </summary>
public StringComparison OptionsComparison { get; set; }

/// <summary>
/// Gets the default value parser provider.
/// <para>
/// The value parsers control how argument values are converted from strings to other types. Additional value
/// parsers can be added so that domain specific types can converted. In-built value parsers can also be replaced
/// for precise control of all type conversion.
/// </para>
/// <remarks>
/// Value parsers are currently only used by the Attribute API.
/// </remarks>
/// </summary>
public ValueParserProvider ValueParsers { get; private set; }

/// <summary>
/// <para>
/// Defines the working directory of the application. Defaults to <see cref="Directory.GetCurrentDirectory"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ private void AddArgument(PropertyInfo prop,

if (argument.MultipleValues)
{
var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType);
var collectionParser = CollectionParserProvider.Default.GetParser(
prop.PropertyType,
convention.Application.ValueParsers);
if (collectionParser == null)
{
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
Expand All @@ -115,7 +117,7 @@ private void AddArgument(PropertyInfo prop,
}
else
{
var parser = ValueParserProvider.Default.GetParser(prop.PropertyType);
var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType);
if (parser == null)
{
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
switch (option.OptionType)
{
case CommandOptionType.MultipleValue:
var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType);
var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers);
if (collectionParser == null)
{
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
Expand All @@ -62,7 +62,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
setter.Invoke(context.ModelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values)));
break;
case CommandOptionType.SingleOrNoValue:
var valueTupleParser = ValueTupleParserProvider.Default.GetParser(prop.PropertyType);
var valueTupleParser = ValueTupleParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers);
if (valueTupleParser == null)
{
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
Expand All @@ -71,7 +71,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
setter.Invoke(context.ModelAccessor.GetModel(), valueTupleParser.Parse(option.HasValue(), option.LongName, option.Value())));
break;
case CommandOptionType.SingleValue:
var parser = ValueParserProvider.Default.GetParser(prop.PropertyType);
var parser = context.Application.ValueParsers.GetParser(prop.PropertyType);
if (parser == null)
{
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
Expand Down
8 changes: 4 additions & 4 deletions src/CommandLineUtils/Internal/CollectionParserProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using McMaster.Extensions.CommandLineUtils.ValueParsers;
using McMaster.Extensions.CommandLineUtils.Abstractions;

namespace McMaster.Extensions.CommandLineUtils
{
Expand All @@ -16,12 +16,12 @@ private CollectionParserProvider()

public static CollectionParserProvider Default { get; } = new CollectionParserProvider();

public ICollectionParser GetParser(Type type)
public ICollectionParser GetParser(Type type, ValueParserProvider valueParsers)
{
if (type.IsArray)
{
var elementType = type.GetElementType();
var elementParser = ValueParserProvider.Default.GetParser(elementType);
var elementParser = valueParsers.GetParser(elementType);
if (elementParser == null)
{
return null;
Expand All @@ -35,7 +35,7 @@ public ICollectionParser GetParser(Type type)
{
var typeDef = type.GetGenericTypeDefinition();
var elementType = typeInfo.GetGenericArguments().First();
var elementParser = ValueParserProvider.Default.GetParser(elementType);
var elementParser = valueParsers.GetParser(elementType);

if (typeof(IList<>) == typeDef
|| typeof(IEnumerable<>) == typeDef
Expand Down
Loading