Skip to content

How to override default parser for a specific TimeSpan option? #559

Description

@MichalMetzger

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?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions