dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.37k stars 378 forks source link

Fluent interface for System.CommandLine #540

Open EifelMono opened 5 years ago

EifelMono commented 5 years ago

Hello everybody,

I have written a fluent interface for the System.CommandLine

Below is the sample from your wiki in fluent form.

static Task<int> Main(string[] args)
    => args.ArgsCommandBuilder("My sample app")
        .Option<int>("--int-option", 42, "An option whose argument is parsed as an int")
        .Option<bool>("--bool-option", default, "An option whose argument is parsed as a bool")
        .Option<FileInfo>("--file-option", default, "An option whose argument is parsed as a FileInfo")
        .OnRootCommand((intOption, boolOption, fileOption) =>
        {
            Console.WriteLine($"The value for --int-option is: {intOption}");
            Console.WriteLine($"The value for --bool-option is: {boolOption}");
            Console.WriteLine($"The value for --file-option is: {fileOption?.FullName ?? "null"}");
        })
        .RunAsync();

More doc, code, tests and samples can be found on my github side

I would like to bring this in these project if you think it is useful for others and fits in this project

jonsequitur commented 5 years ago

Thanks, @EifelMono! It's great to see people experimenting with the API.

Your fluent interface falls into the category of app models that build on the core System.CommandLine library. Enabling people to build APIs like this is a goal of ours, but we're not currently including them in the this repo.

@KathleenDollard and I would be particularly interested to hear about your experience building on the core API.

BenjaminHolland commented 4 years ago

Another option is using configuration callbacks combined with fluent builders. Topshelf is a good example of this in the .net world, and kotlin builders are essentially this with some syntactic sugar.

kzu commented 3 years ago

How would the team feel about just returning "this" from each of the Command methods to allow easy chaining? For example:

var root = new RootCommand()
  .AddCommand(
     new Command("foo")
        .AddArgument(...)
        .AddOption(...))
  .AddCommand(
    new Command("bar")
      ....)
);

That alone would make the API much more fluent-able than it currently is, since everything is void right now...

FWIW, it's probably not obvious enough that you can also use the existing API in a more declarative construction style:

var root = new RootCommand
{
    new Command("add")
    {
        new Argument<string>("--name", () => config.GetString("cli", "name")!, "The name"),
        new Option<long?>(new[] { "--size", "-s" }, () => config.GetNumber("cli", "size")!, "Default size"),
        new Option<bool?>(new[] { "--enabled", "-e" }, () => config.GetBoolean("cli", "enabled"), "Enable?")
    }, 
    new Command("remove")
    {
       // more args/options
    }
};
formicant commented 2 years ago

it's probably not obvious enough that you can also use the existing API in a more declarative construction style:

One cannot provide a CommandHandler alongside some arguments or options using this declarative approach:.

static readonly RootCommand RootCommand = new()
{
    Handler = CommandHandler.Create(Add),
    Arguments = { new Argument<int>("number") }
    // Error: IReadOnlyList<Argument> does not contain a definition for Add
};
static readonly RootCommand RootCommand = new()
{
    Handler = CommandHandler.Create(Add),
    Arguments = new[] { new Argument<int>("number") }
    // Error: Property Arguments cannot be assigned to — it is read-only
};

Is it possible to add Command and RootCommand constructors allowing initializing of the Handler property?

jonsequitur commented 2 years ago

It is possible, but as we work out how to make an easy-to-use source generator, these two things might not go well together.

Here's the current state of the source generator: https://github.com/dotnet/command-line-api/pull/1498

It lets you write code like this and generates the plumbing to bind the arguments:

            void Execute(string fullnameOrNickname, IConsole console, int age)
            {
                boundName = fullnameOrNickname;
                boundConsole = console;
                boundAge = age;
            }

            var nameArgument = new Argument<string>();
            var ageOption = new Option<int>("--age");

            var command = new Command("command")
            {
                nameArgument,
                ageOption
            };

            command.SetHandler(Execute, nameArgument, ageOption);

You wouldn't be able to pass in the relevant arguments and options if the handler were in the constructor and you were also using collection initializer syntax.

This might be a reason to have the SetHandler methods which the generator targets return the command in a fluent style.

@Keboo

Keboo commented 2 years ago

The other thing to consider with this is where else you might need access to those arguments/options that get added to the command. We have recently been moving the string-based matching to the delegate parameters into its own library. This had a performance hit as well as the source of confusion for many people. For the core library, the primary way to retrieve the values that were passed on the command line is to use the ParseResult.ValueFor* methods and pass in the Argument/Option.

With the source generator, it uses the SetHandler method as a trigger to know what to generate. Because the types need to be known at compile-time, the current implementation would not work well with a fluent syntax (will need to think more on how that might work).

What I suspect, is that a fluent style syntax is likely something that would end up being similar to the string-based naming convention, or source generator, where it is an additional library on top of the core library. I would imagine the critical pieces will be figuring out the link between a command's version child symbols (arguments/options) and the command's handler. The examples above appear to rely on the string-based matching (which is fine). The other thing we have been playing around with are ways to make declaring the parser (the commands + options + arguments) in a simpler way (sort of side stepping the request here). One implementation you can see is DragonFruit (which has its own set of limitations as well).