Skip to content

How to use IConsole, global options, and complex parameter types with SetHandler? #1988

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

Closed
cthompson92 opened this issue Dec 9, 2022 · 2 comments

Comments

@cthompson92
Copy link

System.CommandLine version: 2.0.0-beta4.22272.1

Looking through the repo docs, I am seeing references to CommandHandler.Create, which seems like it has been removed in favor of using command.SetHandler.

However, the new SetHandler API does not seem to enable this example in the docs, where a custom, complex type is bound to a single handler parameter from two or more command options.

The SetHandler API also does not seem to enable this example from the docs, where IConsole is injected automatically.

The crux of the issues with these 2 examples (besides the docs still referencing CommandHandler.Create) is the fact that the SetHandler api only allows for command handlers taking these types of parameter sets:
0 parameters
1 InvocationContext parameter
1 parameter per Option which corresponds to a T1, T2, etc type parameter

This lead me to initially defining a handler with 1 parameter per option, but I quickly ran out of overloads which allowed this. I did find some guidance here, which indicates that this issue can be solved by creating a complex type to which allows for multiple Options to map to a single handler parameter. However, this approach will not compile.

My specific use case is one where I have a few commands which have specific options, but I also have a set of global options - such as the Verbosity option, as well as a Dry Run option. I would like to be able to access those options via a Complex Type which I would define to house all of these possible global options, such as:

public class GlobalOptions
{
    public bool DryRun { get; }
    public Verbosity Verbosity { get; } // enum 
    // other options omitted for brevity
   
    public GlobalOptions(bool dryRun, Verbosity verbosity)
    {
        DryRun = dryRun;
        Verbosity = verbosity;
    }
}

This, too, does not seem possible with the SetHandler API, as there would be multiple Global options, but they would be bound to a single parameter for my handler:

// somewhere
public static Option<bool> DryRunOption = new Option<bool>(...);
public static Option<Verbosity> VerbosityOption = new Option<Verbosity>(...);

var root = new RootCommand();
root.AddGlobalOption(DryRunOption);
root.AddGlobalOption(VerbosityOption);

var subCommand = new Command()
{
    subCommandOption1,
    subCommandOption2,
    // ...
};

subCommand.SetHandler(
    (GlobalOptions globalOptions, object opt1, object opt2) =>
    {
        // stuff
    },
    subCommandOption1,
    subCommandOption2
);

root.AddCommand(subCommand);

root.Invoke(args);

I also attempted to go a step further, and define a class which would be used for all of my sub commands arguments, but this runs into the same issue of signature mismatch:

public class MyCommandArgs
{
    public GlobalOptions GlobalOptions { get; }
    public object MyOption1 { get; }
    public object MyOption2 { get; }
    public IConsole Console { get; } // System.CommandLine type
    
    public MyCommandArgs(GlobalOptions globalOptions, object myOption1, object myOption2, IConsole console)
    {
        GlobalOptions = globalOptions;
        MyOption1 = myOption1;
        MyOption2 = myOption2;
        Console = console;
    }
}

Also, I'm not certain this would even work, as based on this comment, this may fall into the 'recursive' category.

TLDR:

  1. How would one access parseResult and/or IConsole in a command handler using SetHandler?
  2. How would one properly bind multiple Options to a single Complex Type using the SetHandler API?
@elgonzo
Copy link
Contributor

elgonzo commented Dec 9, 2022

How would one access parseResult and/or IConsole in a command handler using SetHandler?

There are SetHandler overloads for handlers receving an InvocationContext:

Handler.SetHandler(this Command command, Action<InvocationContext> handle)
Handler.SetHandler(this Command command, Func<InvocationContext, Task> handle)

The ParseResult as well as the IConsole are provided by the InvocationContext. As you might notice, you can't put both options/arguments and InvocationContext together as handler parameters (which also has received comments in the past).

How would one properly bind multiple Options to a single Complex Type using the SetHandler API?

If you want complex option model types, it's probably better to use the System.CommandLine.NamingConventionBinder package, as the page with the examples you linked to mentions at the very beginning. Note however that this package, due to it relying on reflection, will not provide (easy) support for trimming and/or native AOT compiling, if that is of interest to you...

(P.S.: Be careful about relying on old-ish comments/threads. The library is still in active and heavy development and has undergone substantial changes in the last two years or so. Basically, some explanations/comments older than one or two years might not fully apply to the current/latest System.CommandLine version(s) anymore...)

@cthompson92
Copy link
Author

Thanks, @elgonzo. Between your reply and some additional documentation I was reading, I have gotten past this issue, and am going to mark it as closed.

A rough outline of how the CLI I am building works now:
There are ~6 global command line options which are defined as part of the Root Command:

public class MyRootCommand : RootCommand
    public static readonly Option<bool> DryRunGlobalOption = ...;
    // other options

    public MyRootCommand(string name) : base(name, "...")
    {
        AddGlobalOption(DryRunGlobalOption);
        // ...
    }

Individual commands will define their own options and access them and/or the global options. Due to the sheer number of options I have, I need to utilize the SetHandler overload which provides the InvocationContext:

public class MyCommand1 : Command
    public static readonly Option<bool> SwitchOption1 = ...;
    // other options

    public MyCommand1(string name) : base(name, "...")
    {
        AddOption(SwitchOption1);
        // ...

        SetHandler(Foo);
    }

    private void Foo(InvocationContext context)
    {
        var globalOptions = new GlobalOptions(
            context.ParseResult.GetValueForOption(MyRootCommand.DryRunGlobalOption),
             ...);

        var switch1 = context.ParseResult.GetValueForOption(SwitchOption1);
        // ... gather other options
        Bar(context.Console, globalOptions, switch1, ...);
    }

    private void Bar(IConsole console, GlobalOptions globalOptions, bool switch1, ...)
    {
        // implementation
    }

This is more than sufficient for my purposes, without needing to delve into the System.CommandLine.NamingConventionBinder package at all.

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

No branches or pull requests

2 participants