-
-
Notifications
You must be signed in to change notification settings - Fork 263
Description
I'm using McMaster.Extensions.CommandLineUtils (v4.1.1 on .NET 9) in C# to parse command-line arguments. I have a model class with properties decorated with the [Option] attribute.
One of my properties is a nullable TimeSpan:
class MyClass
{
[Option("--timeout", CommandOptionType.SingleOrNoValue)]
public TimeSpan? Timeout { get; }
// ...
}
The library has a built-in parser for TimeSpan, but my Timeout property needs to accept a unique string format that the default parser doesn't handle. I want to provide a custom parser only for this specific option, without affecting other TimeSpan properties in my project.
I have tried two different approaches, and both have failed.
Attempt 1: Implementing IValueParser
First, I created a custom parser that implements IValueParser<TimeSpan?>:
private sealed class MyTimeSpanParser : IValueParser<TimeSpan?>
{
public Type TargetType => typeof(TimeSpan?);
public TimeSpan? Parse(string? argumentName, string? value, CultureInfo culture)
{
// ...
}
object? IValueParser.Parse(string? argumentName, string? value, CultureInfo culture)
{
return this.Parse(argumentName, value, culture);
}
}
Then, I registered it in my main application setup:
var commandLineApplication = new CommandLineApplication<MyClass>();
commandLineApplication.Conventions.UseDefaultConventions();
commandLineApplication.ValueParsers.AddOrReplace(new MyTimeSpanParser());
commandLineApplication.Parse(args);
// ...
When I run the app with --timeout 15s, I get a FormatException: '15s' is not a valid value for TimeSpan.
When I debug the library's code, I can see that
var parser = context.Application.ValueParsers.GetParser(prop.PropertyType);
returns the default TimeSpanConverter, not my custom MyTimeSpanParser.
As a test, I created a new wrapper class class MyTimeSpan { public TimeSpan? Value { get; } ... }. When I changed my property and parser to use this new MyTimeSpan type, my custom parser was used correctly. This seems to indicate that my parser is ignored for built-in types but works for new custom types. However, creating a wrapper class just for this is not an ideal solution.
Attempt 2: Using [TypeConverter]
Following the discussion in GitHub Issue #62, I tried using a custom TypeConverter.
First, I updated my model class:
class MyClass
{
[Option("--timeout", CommandOptionType.SingleOrNoValue)]
[TypeConverter(typeof(MyTimeSpanConverter))]
public TimeSpan? Timeout { get; }
// ...
}
And I created the custom converter:
class MyTimeSpanConverter : TimeSpanConverter
{
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string stringValue)
{
//...
}
return base.ConvertFrom(context, culture, value);
}
}
My main application setup for this attempt did not register any parsers manually, relying only on the conventions:
var commandLineApplication = new CommandLineApplication<MyClass>();
commandLineApplication.Conventions.UseDefaultConventions();
commandLineApplication.Parse(args);
// ...
I get the exact same FormatException as in my first attempt. The TypeConverter attribute seems to be ignored, and the default TimeSpanConverter is used.
As another test, I tried registering my converter globally at startup with TypeDescriptor.AddAttributes(...). This did work and my converter was called. However, this is a global change that affects the entire application, and my requirement is to apply this logic only to one specific property.
What am I missing in these two approaches? How can I correctly instruct CommandLineUtils to use a custom parser for a specific option of a built-in type like TimeSpan?, without affecting other TimeSpan properties and without creating a wrapper class?