Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does the Options attribute class really need to be sealed? #859

Open
SJFriedl opened this issue Nov 26, 2022 · 3 comments
Open

Does the Options attribute class really need to be sealed? #859

SJFriedl opened this issue Nov 26, 2022 · 3 comments

Comments

@SJFriedl
Copy link

SJFriedl commented Nov 26, 2022

I have been using CommandLineParser for a while now (and it's changed how I build tools -- thank you!) including in a program with ~50 Verbs (and growing), and since many of the Verbs take common options, I was hoping to extend Options to provide common handling of them.

Simple example: if 15 Verbs take a LogFile option, each of which has a long name ("log-file") and a help description, I can do this now by constant strings to keep them consistent, but if I were able to do:

[AttributeUsage(AttributeTargets.Property)]
public class LogFileOptionAttribute : OptionAttribute
{
    public LogFileOptionAttribute(bool required = false)
        : base("log-file") // I don't use short option names for anything
    {
        HelpText = "Name of the logfile to use for blah blah blah";
        Required = required;
    }
}

and then adorn the actual option with:

public class BlahOptions {
    ...
    [LogFileOption]
    public string LogFileName {get ; set; }
    ...
}

I have easily a dozen of these common options. It's not a huge deal to create constant strings:

    [Option(OPTION_LOGFILE_NAME, HelpText = OPTION_LOGFILE_HELPTEXT)]

but adding this level of abstraction would be more convenient.

If there's a good reason for this class to be sealed, then so be it, but mine doesn't feel like a ridiculous use case. I may well get to 100 verbs for this tool by the time I'm done.

@ericnewton76
Copy link
Member

The core code of the parser is only looking for "OptionsAttribute" not "LogFileOptionAttribute" types...

Submit a pull request with some initial work to enable this feature and we would consider it.

@SJFriedl
Copy link
Author

I think reflection looking for an attribute will/can get derived classes of that attribute as well, but this is clearly on me to demonstrate. Will dig in.

@SJFriedl
Copy link
Author

SJFriedl commented Jun 11, 2023

I finally got around to digging in, and after making literally one change - remove sealed from OptionAttribute, this all works super well.

The current code does in fact look for derived types because in Core/ReflectionExtensions.cs the GetSpecifications<T> method uses GetCustomAttributes with a true parameter: this is the inherit flag. The MSFT docs say it looks for ancestors but I believe it's actually looking for descendant classes. Maybe I'm confused about the terminology.

In any case, using (say) [LogFileOption] works great defining the long name (--log-file) and the help text in a common way, and one can still adorn it with Required=t/f flag in the usual way. This doesn't appear to impact the normal use of options, they all work as expected.

This also works, surprisingly, when LogFileOptionAttribute is defined in a different assembly than the option class that refers to it - reflection sometimes has guardrails on just how far it's going to look for things. I have a common library of supporting code that feeds multiple projects, including the command-line support.

The VerbAttribute is already non-sealed - the code shows this as having been considered but commented out - and I did not change the sealed value for ValueAttribute because I didn't need to extend it, and wanted to keep the changes limited to one thing. But I think that removing sealed from ValueAttribute is probably worth doing as well.

I'm doing my dev in Visual Studio 2022, with a large suite of C# .NET 6 code, and I compiled the CommandLine stuff with netstandard2.0; I have no real experience with cross-framework development so don't know the ins and outs of mixing and matching, and of course have not considered the F# stuff at all. I'm running my code on Windows 11.

Overall, it all Just Works(tm), and I haven't found a downside or gotcha yet.

I'm going to keep using this for a little while to shake it out, then will see if I can figure out how to make a pull request to make it so.

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

No branches or pull requests

2 participants