Skip to content

[Discussion] Designing a simpler command setup API #1776

Closed
@jonsequitur

Description

@jonsequitur

Over the course of beta 2 and beta 4, a couple of conventions were removed because they'd led to too much confusion. We still haven't found the right balance of simplicity and composability, though. The conversation has come up in a number of different issues, so let's use this one to focus the discussion and refine the design. I'll post a few proposals below.

Recap

  • In beta 2, we removed CommandHandler.Create from System.CommandLine (and moved it to System.CommandLine.NamingConventionBinder) because its use of naming convention wasn't obvious, and naming mismatches between option or argument names and their associated handler parameters became the most common source of problems people had while trying to use the API. (It was also a reflection-heavy API and needed to be removed in order to support trimming and NativeAOT compilation.) We introduced the Command.SetHandler extension methods as a replacement.

  • By beta 4, a problem with a different convention had become apparent in SetHandler, which was that its params IValueDescriptor[] parameter, designed to handle binding parameters from either the ParseResult or ServiceProvider, was confusing people as well. SetHandler could be called without passing any options or arguments to bind to, leading it to try to resolve everything from the service provider, which is the less common case. The fix in beta 4 was to change the params parameter to a IValueDescriptor<T> for each generic parameter. This made code a little less verbose because it plays more nicely with C# type inference. But removing the service provider fallback led to more ceremony for accessing injected objects.

Current SetHandler problems

1. Verbosity

The most common criticism now of SetHandler is that it requires too much ceremony to use. The problem is illustrated by this code snippet, which needs to reference option three times:

var option = new Option<int>("-i"); // 1

var command = new RootCommand
{
    option // 2 
};

command.SetHandler(i => { }, option); // 3

An API that composes these together is needed, but we hope to avoid too much coupling between the handling and parsing APIs. The parser API defines the grammar with a good deal of flexibility, while the handler API defines how to bind symbols regardless of their position in the parsed input. A convenience method that sets up both at once will likely prevent people from creating specific parser configurations, but as these won't be the majority case, a convenience API is warranted.

2. Service provider access is unclear

In the beta2 version of SetHandler, parameters of commonly-used types such as ParseResult, CancellationToken, and IConsole would be implicitly bound from the service provider. The beta 4 version requires that the IValueDescriptor<T> parameter count match the generic type parameter count (e.g. public static void SetHandler<T1, T2>(this Command command, Action<T1, T2> handle, IValueDescriptor<T1> symbol1, IValueDescriptor<T2> symbol2). This makes it unclear what to pass to get instances of these other types. This can be done by implementing a custom BinderBase<T> but this also requires too much ceremony.

3. Directly returning exit codes isn't supported (#1570)

A detailed explanation is in the comment here: #1570 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions