Skip to content

Commit dc17772

Browse files
feature: support TypeConverter for parsing types like GUID, DateTime, and others as command line options and (#345)
1 parent c18ac70 commit dc17772

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
namespace McMaster.Extensions.CommandLineUtils.Abstractions
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Diagnostics;
7+
using System.Globalization;
8+
using System.Text;
9+
10+
/// <summary>
11+
/// A factory creating generic implementations of <see cref="IValueParser{T}"/>. The implementations are based
12+
/// on automatically located <see cref="TypeConverter"/> classes that are suitable for parsing.
13+
/// </summary>
14+
sealed class DefaultValueParserFactory
15+
{
16+
const int DefaultMaxCacheCapacity = 100;
17+
18+
#region Private Fields
19+
20+
private readonly int _maxCacheCapacity;
21+
22+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
23+
readonly Dictionary<Type, IValueParser> _parsersByTargetType = new Dictionary<Type, IValueParser>();
24+
25+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
26+
readonly HashSet<Type> _notSupportedTypes = new HashSet<Type>();
27+
28+
#endregion
29+
30+
public DefaultValueParserFactory(int maxCacheCapacity = DefaultMaxCacheCapacity)
31+
{
32+
_maxCacheCapacity = maxCacheCapacity;
33+
if (_maxCacheCapacity < 1) throw new ArgumentOutOfRangeException(nameof(maxCacheCapacity));
34+
}
35+
36+
[DebuggerStepThrough]
37+
public bool TryGetParser<T>(out IValueParser<T> parser)
38+
{
39+
if (TryGetParser<T>(out IValueParser generalizedParser))
40+
{
41+
parser = (IValueParser<T>)generalizedParser;
42+
return true;
43+
}
44+
45+
parser = null;
46+
return false;
47+
}
48+
49+
public bool TryGetParser<T>(out IValueParser parser)
50+
{
51+
var targetType = typeof(T);
52+
if (_notSupportedTypes.Contains(targetType))
53+
{
54+
parser = null;
55+
return false;
56+
}
57+
58+
if (_parsersByTargetType.TryGetValue(targetType, out parser))
59+
{
60+
Debug.Assert(targetType == parser.TargetType);
61+
return true;
62+
}
63+
64+
var converter = TypeDescriptor.GetConverter(targetType);
65+
if (converter.CanConvertFrom(typeof(string)))
66+
{
67+
if (_parsersByTargetType.Count >= _maxCacheCapacity)
68+
_parsersByTargetType.Clear();
69+
parser = new TypeConverterValueParser<T>(targetType, converter);
70+
_parsersByTargetType[targetType] = parser;
71+
Debug.Assert(_parsersByTargetType.Count <= _maxCacheCapacity);
72+
return true;
73+
}
74+
75+
parser = null;
76+
if (_notSupportedTypes.Count > _maxCacheCapacity)
77+
_notSupportedTypes.Clear();
78+
_notSupportedTypes.Add(targetType);
79+
Debug.Assert(_notSupportedTypes.Count <= _maxCacheCapacity);
80+
return false;
81+
}
82+
83+
public IValueParser<T> GetParser<T>()
84+
{
85+
return TryGetParser<T>(out IValueParser<T> converter)
86+
? converter
87+
: throw new NotSupportedException(
88+
new StringBuilder($"No suitable type converter found for {typeof(T)}.")
89+
.Append($" Make sure a type converter capable of parsing {typeof(string)} to {typeof(T)} exists and is discoverable.")
90+
.Append($" Did you forget to annotate the target type with {typeof(TypeConverterAttribute)}?")
91+
.ToString());
92+
}
93+
94+
private sealed class TypeConverterValueParser<T> : IValueParser<T>
95+
{
96+
public TypeConverterValueParser(Type targetType, TypeConverter typeConverter)
97+
{
98+
TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
99+
TypeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter));
100+
}
101+
102+
public Type TargetType { get; }
103+
104+
private TypeConverter TypeConverter { get; }
105+
106+
public T Parse(string argName, string value, CultureInfo culture)
107+
{
108+
try
109+
{
110+
culture ??= CultureInfo.InvariantCulture;
111+
return (T)TypeConverter.ConvertFromString(null, culture, value);
112+
}
113+
catch (ArgumentException e)
114+
{
115+
throw new FormatException(e.Message, e);
116+
}
117+
}
118+
119+
object IValueParser.Parse(string argName, string value, CultureInfo culture) => Parse(argName, value, culture);
120+
}
121+
122+
}
123+
}

src/CommandLineUtils/Abstractions/ValueParserProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions
1515
public class ValueParserProvider
1616
{
1717
private readonly Dictionary<Type, IValueParser> _parsers = new Dictionary<Type, IValueParser>(10);
18+
private readonly DefaultValueParserFactory _defaultValueParserFactory = new DefaultValueParserFactory();
1819

1920
internal ValueParserProvider()
2021
{
@@ -100,6 +101,9 @@ public IValueParser GetParser(Type type)
100101
return EnumParser.Create(type);
101102
}
102103

104+
if (_defaultValueParserFactory.TryGetParser<T>(out parser))
105+
return parser;
106+
103107
if (ReflectionHelper.IsNullableType(type, out var wrappedType) && wrappedType != null)
104108
{
105109
if (wrappedType.IsEnum)

test/CommandLineUtils.Tests/ValueParserProviderTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ private class Program
110110

111111
[Option("--timespan")]
112112
public TimeSpan TimeSpan { get; }
113+
114+
[Option("--guid", CommandOptionType.SingleValue)]
115+
public Guid Guid { get; }
116+
117+
[Option("--guid-opt", CommandOptionType.SingleValue)]
118+
public Guid? GuidOpt { get; }
113119
}
114120

115121
private sealed class InCulture : IDisposable
@@ -480,6 +486,35 @@ public void ParsesBoolArray(int repeat)
480486
Assert.All(parsed.Flags, value => Assert.True(value));
481487
}
482488

489+
[Theory]
490+
[InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")]
491+
[InlineData("ff23ef12500a48df9a5d151c2adc2a0a")]
492+
[InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")]
493+
[InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")]
494+
[InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")]
495+
public void ParsesGuid(string arg)
496+
{
497+
var expected = Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a");
498+
var parsed = CommandLineParser.ParseArgs<Program>("--guid", arg);
499+
Assert.Equal(expected, parsed.Guid);
500+
}
501+
502+
[Theory]
503+
[InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")]
504+
[InlineData("ff23ef12500a48df9a5d151c2adc2a0a")]
505+
[InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")]
506+
[InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")]
507+
[InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")]
508+
[InlineData("")]
509+
public void ParsesGuidNullable(string arg)
510+
{
511+
var expected = String.IsNullOrWhiteSpace(arg)
512+
? (Guid?)null
513+
: Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a");
514+
var parsed = CommandLineParser.ParseArgs<Program>("--guid-opt", arg);
515+
Assert.Equal(expected, parsed.GuidOpt);
516+
}
517+
483518
[Theory]
484519
[InlineData(nameof(Program.Float), "--float", "123.456,7", "de-DE", 123456.7f)]
485520
[InlineData(nameof(Program.Double), "--double", "123.456,789", "de-DE", 123456.789)]

0 commit comments

Comments
 (0)