diff --git a/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs b/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs
new file mode 100644
index 00000000..41a75eda
--- /dev/null
+++ b/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs
@@ -0,0 +1,123 @@
+namespace McMaster.Extensions.CommandLineUtils.Abstractions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Globalization;
+ using System.Text;
+
+ ///
+ /// A factory creating generic implementations of . The implementations are based
+ /// on automatically located classes that are suitable for parsing.
+ ///
+ sealed class DefaultValueParserFactory
+ {
+ const int DefaultMaxCacheCapacity = 100;
+
+ #region Private Fields
+
+ private readonly int _maxCacheCapacity;
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ readonly Dictionary _parsersByTargetType = new Dictionary();
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ readonly HashSet _notSupportedTypes = new HashSet();
+
+ #endregion
+
+ public DefaultValueParserFactory(int maxCacheCapacity = DefaultMaxCacheCapacity)
+ {
+ _maxCacheCapacity = maxCacheCapacity;
+ if (_maxCacheCapacity < 1) throw new ArgumentOutOfRangeException(nameof(maxCacheCapacity));
+ }
+
+ [DebuggerStepThrough]
+ public bool TryGetParser(out IValueParser parser)
+ {
+ if (TryGetParser(out IValueParser generalizedParser))
+ {
+ parser = (IValueParser)generalizedParser;
+ return true;
+ }
+
+ parser = null;
+ return false;
+ }
+
+ public bool TryGetParser(out IValueParser parser)
+ {
+ var targetType = typeof(T);
+ if (_notSupportedTypes.Contains(targetType))
+ {
+ parser = null;
+ return false;
+ }
+
+ if (_parsersByTargetType.TryGetValue(targetType, out parser))
+ {
+ Debug.Assert(targetType == parser.TargetType);
+ return true;
+ }
+
+ var converter = TypeDescriptor.GetConverter(targetType);
+ if (converter.CanConvertFrom(typeof(string)))
+ {
+ if (_parsersByTargetType.Count >= _maxCacheCapacity)
+ _parsersByTargetType.Clear();
+ parser = new TypeConverterValueParser(targetType, converter);
+ _parsersByTargetType[targetType] = parser;
+ Debug.Assert(_parsersByTargetType.Count <= _maxCacheCapacity);
+ return true;
+ }
+
+ parser = null;
+ if (_notSupportedTypes.Count > _maxCacheCapacity)
+ _notSupportedTypes.Clear();
+ _notSupportedTypes.Add(targetType);
+ Debug.Assert(_notSupportedTypes.Count <= _maxCacheCapacity);
+ return false;
+ }
+
+ public IValueParser GetParser()
+ {
+ return TryGetParser(out IValueParser converter)
+ ? converter
+ : throw new NotSupportedException(
+ new StringBuilder($"No suitable type converter found for {typeof(T)}.")
+ .Append($" Make sure a type converter capable of parsing {typeof(string)} to {typeof(T)} exists and is discoverable.")
+ .Append($" Did you forget to annotate the target type with {typeof(TypeConverterAttribute)}?")
+ .ToString());
+ }
+
+ private sealed class TypeConverterValueParser : IValueParser
+ {
+ public TypeConverterValueParser(Type targetType, TypeConverter typeConverter)
+ {
+ TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
+ TypeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter));
+ }
+
+ public Type TargetType { get; }
+
+ private TypeConverter TypeConverter { get; }
+
+ public T Parse(string argName, string value, CultureInfo culture)
+ {
+ try
+ {
+ culture ??= CultureInfo.InvariantCulture;
+ return (T)TypeConverter.ConvertFromString(null, culture, value);
+ }
+ catch (ArgumentException e)
+ {
+ throw new FormatException(e.Message, e);
+ }
+ }
+
+ object IValueParser.Parse(string argName, string value, CultureInfo culture) => Parse(argName, value, culture);
+ }
+
+ }
+}
diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs
index 5e5e2e1d..4206d6e7 100644
--- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs
+++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs
@@ -15,6 +15,7 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions
public class ValueParserProvider
{
private readonly Dictionary _parsers = new Dictionary(10);
+ private readonly DefaultValueParserFactory _defaultValueParserFactory = new DefaultValueParserFactory();
internal ValueParserProvider()
{
@@ -100,6 +101,9 @@ public IValueParser GetParser(Type type)
return EnumParser.Create(type);
}
+ if (_defaultValueParserFactory.TryGetParser(out parser))
+ return parser;
+
if (ReflectionHelper.IsNullableType(type, out var wrappedType) && wrappedType != null)
{
if (wrappedType.IsEnum)
diff --git a/test/CommandLineUtils.Tests/ValueParserProviderTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderTests.cs
index b48676ae..c21e2241 100644
--- a/test/CommandLineUtils.Tests/ValueParserProviderTests.cs
+++ b/test/CommandLineUtils.Tests/ValueParserProviderTests.cs
@@ -110,6 +110,12 @@ private class Program
[Option("--timespan")]
public TimeSpan TimeSpan { get; }
+
+ [Option("--guid", CommandOptionType.SingleValue)]
+ public Guid Guid { get; }
+
+ [Option("--guid-opt", CommandOptionType.SingleValue)]
+ public Guid? GuidOpt { get; }
}
private sealed class InCulture : IDisposable
@@ -480,6 +486,35 @@ public void ParsesBoolArray(int repeat)
Assert.All(parsed.Flags, value => Assert.True(value));
}
+ [Theory]
+ [InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")]
+ [InlineData("ff23ef12500a48df9a5d151c2adc2a0a")]
+ [InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")]
+ [InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")]
+ [InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")]
+ public void ParsesGuid(string arg)
+ {
+ var expected = Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a");
+ var parsed = CommandLineParser.ParseArgs("--guid", arg);
+ Assert.Equal(expected, parsed.Guid);
+ }
+
+ [Theory]
+ [InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")]
+ [InlineData("ff23ef12500a48df9a5d151c2adc2a0a")]
+ [InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")]
+ [InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")]
+ [InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")]
+ [InlineData("")]
+ public void ParsesGuidNullable(string arg)
+ {
+ var expected = String.IsNullOrWhiteSpace(arg)
+ ? (Guid?)null
+ : Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a");
+ var parsed = CommandLineParser.ParseArgs("--guid-opt", arg);
+ Assert.Equal(expected, parsed.GuidOpt);
+ }
+
[Theory]
[InlineData(nameof(Program.Float), "--float", "123.456,7", "de-DE", 123456.7f)]
[InlineData(nameof(Program.Double), "--double", "123.456,789", "de-DE", 123456.789)]