TLDR; I'd like to use attributes to do commandline parameter to complex class property name mapping.
I just ran into the problem of too many parameters in command handler functions.
I found the complex object mapping proposed solution where I pass a config class with matching properties to the command handler.
Now I'm stuck using property names that match command attribute names, i.e. I want the commandline attributes to be concise, but I want my class naming to be descriptive.
Is it possible or planned to use attributes on the property names for matching, similar to e.g. JSON where the attribute describes the JSON node and the class code can be whatever?
I've used this method in other commandline mapping classes, but I'd like to standardize on this version as it seems to have a good chance of becoming a standard.
See: https://github.com/commandlineparser/commandline
E.g.
public class CommandLineOptions
{
[CommandLineProperty("settings")]
public string FooBarSettingsFile { get; set; } = "foo.json";
// Other attributes
}
// ...
Handler = CommandHandler.Create<CommandLineOptions>(PrintInfoCommand);
Using attributes could also allow for defining parse handling, e.g. default value handling, required, optional, etc.
Is this supported, or is there an alternative easy way to instruct the parser which class properties to map to which commandline attributes?
You can decouple the model names from the command line syntax currently, though the approach doesn't use attributes. It's intended also to support building your own conventions, so it might not be as concise as you like:
You can configure the model binder using middleware:
Regarding attributes specifically, one goal of System.CommandLine is to provide a base layer for building conventions like you're describing. You can take a look at some work on these kinds of higher-level APIs here: https://github.com/KathleenDollard/command-line-api-starfruit. We'd appreciate feedback.
This model seems appealing:
https://github.com/KathleenDollard/command-line-api-starfruit/blob/master/StarFruit/Program.cs
Regarding others building upon, agree, but the base needs to be 90% useful to most, and custom code for argument binding seems complex vs. attributes or other methods of self describing binding / option / command code in the base implementation.
Thank you for tagging as enhancement, will follow.
I just tried to start using System.CommandLine and immediately bumped into this. After adding 5 Options it quickly becomes unmanageable.
I need to propagate parsed options to many places in my logic code => inconvenient to have a dozen separate variables => need to encapsulate all variables into one UserOpts object => have to manually maintain the mapping from all Options I add to Commands into that custom UserOpts object.
@chhh Can you give an example of the code? Propagating the Option objects throughout your code is usually unnecessary if you're letting the model binder build your own custom types for you.
@jonsequitur
I didn't know about "model binder". Armed with that new knowledge found some unit tests and came up with the following, which most closely resembles my typical usage of command line parsing, is that what you meant by "letting the model binder build your own custom types for you":
class UserOpts {
public int Size { get; set; }
public string Text { get; set; }
public double Radius { get; set; }
public override string ToString() {
return $"{nameof(Size)}: {Size}, {nameof(Text)}: {Text}, {nameof(Radius)}: {Radius}";
}
}
static RootCommand CreateRootCommand() {
var cmd = new RootCommand("test root command");
cmd.AddOption(new Option<int>(new[] {"-s", "--size"}) {Required = true});
cmd.AddOption(new Option<string>(new[] {"-t", "--text"}) {Required = false});
cmd.AddOption(new Option<double>(new[] {"-r", "--Radius"}) {Required = false});
return cmd;
}
static UserOpts ParseUserOpts(string[] args) {
var parser = new Parser(CreateRootCommand());
var parseResult = parser.Parse(args);
if (parseResult.Errors.Count > 0) {
var msg = String.Join(", ", parseResult.Errors.Select(err => err.Message).ToList());
throw new Exception(msg);
}
var bindingContext = new BindingContext(parseResult);
var binder = new ModelBinder(typeof(UserOpts));
var instance = binder.CreateInstance(bindingContext);
if (!(instance is UserOpts))
throw new InvalidOperationException("didn't get the user object from parser");
return (UserOpts)instance;
}
static void Main(string[] args) {
var opts = ParseUserOpts("-s 42 -t hello".Split(' '));
Console.WriteLine($"Parsed user opts object: {opts}");
}
@ptr727 I've found that if you assign Name property of an Option it can be used for binding to class. All three of the following:
new Option<string>(new[]{"-c", "--ccc"}){Required = false, Name = "binding-by-name"}
new Option<string>(new[]{"-b", "--bbb"}){Required = false}
new Option<string>(new[]{"-a", "--aaa"}){Required = true}
Will bind just fine to a class like:
class Opts {
public String BindingByName { get; set; }
public String B { get; set; }
public String Aaa { get; set; }
}
class OptionAttribute : Attribute
{
private string[] Alias { get; set; }
private string Description { get; set; }
public bool IsHidden { get; set; }
public OptionAttribute(string[] alias, string description = null, bool isHidden = false)
{
Alias = alias;
Description = description;
}
public Option GetOption(Type paramType)
{
var type = typeof(Option<>).MakeGenericType(paramType);
var option = Activator.CreateInstance(type, Alias, Description) as Option;
option.IsHidden = IsHidden;
return option;
}
}
public class Options
{
[Option(new[] { "--languageId", "-languageId" },
"LCID of language to use (if available), e.g. 0x409 for English")]
public int LanguageId { get; set; }
...
}
private IEnumerable<Option> GetOptions<T>()
{
List<Option> options = new List<Option>();
foreach (var pi in typeof(T).GetProperties())
{
var attr = pi.GetCustomAttribute<OptionAttribute>();
if (attr != null)
{
options.Add(attr.GetOption(pi.PropertyType));
}
}
return options;
}
@botcher We're working on approaches like this as an eventual expansion of the DragonFruit model, but using the C#9 source generator feature rather than reflection. We're typically avoided using reflection for configuring the parser due to performance concerns.
You can see more here: https://github.com/KathleenDollard/command-line-api-starfruit
It is great!
Most helpful comment