Skip to content

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

@MichalMetzger

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions