-
Notifications
You must be signed in to change notification settings - Fork 482
Description
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.
Activity
ericnewton76 commentedon Mar 7, 2023
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 commentedon Mar 13, 2023
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 commentedon Jun 11, 2023
I finally got around to digging in, and after making literally one change - remove
sealed
fromOptionAttribute
, this all works super well.The current code does in fact look for derived types because in
Core/ReflectionExtensions.cs
theGetSpecifications<T>
method usesGetCustomAttributes
with atrue
parameter: this is theinherit
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 withRequired=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 forValueAttribute
because I didn't need to extend it, and wanted to keep the changes limited to one thing. But I think that removingsealed
fromValueAttribute
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.