-
Notifications
You must be signed in to change notification settings - Fork 386
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
Proposal: CLI Actions #2071
Comments
This lookup looks type-based rather than name-based. |
Thanks! Updated above. I forgot to add the option names as parameters. The generic type parameters are also needed since in this context, the compiler has no way to know what the correct type is. |
Under this proposal, would it be possible (and/or make sense) to skip the use of CliActions entirely? i.e. something like this: var rootCommand = new RootCommand()
{
new Option<FileInfo>("--input")
};
var result = rootCommand.Parse(args);
// This is essentially the CliAction body, just without the cancellationToken.
Console.WriteLine($"Processing file {parseResult.GetValue<FileInfo>("--input").Name}..."); It seems like this would require fewer concepts and simplify the control flow in very basic scenarios. Good for those just getting started with this API (like me!) Also, consider changing the name of |
Yes, you can just use the |
@jonsequitur I'm reading through this, and I'm having trouble to see why
imo, the |
(And I'll say up front that assigment using a simple lambda will still be possible. And you won't have to pattern match in most cases.) The first two are straighforward:
Ok, here's the part that probably needs a little more explanation:
Here's what this proposal should provide that
We also hope this will also make the API a little more discoverable and progressive as people need more features. Here are the stages we have in mind:
|
If I understand correctly, the reason for I find properties are more discoverable than pattern matching: switch (result.Action)
{
case HelpAction helpAction:
await helpAction.RunAsync();
Console.WriteLine("For more help see https://github.com/dotnet/command-line-api");
break; vs Xyz.HelpHandler = result => {
defaultHelpAction = new HelpAction(result);
defaultHelpAction.Run();
Console.WriteLine("For more help see https://github.com/dotnet/command-line-api");
}; |
Just checking: in this proposal, Why does |
This is true if it's a fixed set, and it's something we discussed a bit. Any thoughts about how interception (examples 2 and 3 in my comment) might work with properties for the different handlers?
Correct. They're redundant concepts.
|
You'll have to forgive me, as I am new at using this library. So, if there is a way to manage this, just let me know. But one thing I am having an issue with is if Command is inherited from in order to make a custom command, and the handler is configured inside the CustomCommand class, the handler is very tightly coupled to the fact that host is used. In the example below, context.GetHost() has to be called in order to get an instance of IPackager. This now makes the InstallCommand itself dependent on knowing how the command line was configured at application startup. It shouldn't need any knowledge of this. When a generic base "Command" is used and configured from the top level, this is not an issue because the application startup code knows whether or not Hosting was used, and therefore can set the handler up correctly.
So, referring to this example, my initial reaction on how to fix this would be to either push the IPackager up to the constructor or the Handler method to the constructor. I don't really like the latter because it puts the burden on the caller to define a handler which defeats the purpose of abstracting it away in a child command. So then the other option is to pass the IPackager in through the constructor and use it as follows:
But, there seems to be no good way of resolving the services from the host in order to use them in the InstallCommand constructor. But I may just be missing something.
I feel as if we should be able to have some way of accessing the Host when creating commands so we can resolve services when constructing commands themselves. |
@Greg-Freeman, the general guidance we've given on using DI with System.CommandLine is to do it as lazily as possible. This applies to both configuration and resolution of dependencies and applies whether or not you're using a host. This is because the parser might be used with no invocation of any
Under the new To reduce unnecessary work on the dependency configuration side, we're also investigating making the instantiation of |
This proposal describes a rethinking of the approach to command invocation that's been in place in System.CommandLine from the beginning. The convention- and reflection-based approach (
CommandHandler.Create(Func<...>)
and overloads) was removed in past previews in favor of something suited to better performance, trimming, and NativeAOT compilation (Command.SetHandler(Func<...>)
and overloads). This change reduced the usability of the API by increasing its verbosity. The current proposal is for a design to replace these APIs with something quite different. (For more background and discussion, see #1776.)A few examples
I'll start with a few code examples before discussing the various specific goals of the design.
The core concept adds a new abstraction, roughly similar to the existing
ICommandHandler
, called aCliAction
. The core of the interface looks like this:(The parameters to
RunAsync
assume that theInvocationContext
class will be combined intoParseResult
, a change currently under discussion.)A
CliAction
can be attached to a command (or other symbol--more on that below) very much like aCommandHandler
today:For convenience, the action can be set up using a delegate, much like today:
After parsing, you can run the action designated by the command line input like this:
Just like the existing
InvokeAsync
APIs, this will run the code associated with the command the end user specified.While there are a number of similarities to the current command handler API, it's intended to be used somewhat differently, using different
CliAction
types to represent outcomes that you can inspect and alter prior to invocation.The key difference can be summed up as:
The
ParseResult.Action
property, set during parsing, makes the behavior designated by the parsed command line explicit, inspectable, and configurable prior to invocation.Goals:
Clearly separate parsing from invocation.
The
Parse
andInvoke
methods currently appear as sibling methods onParser
andCommand
, which, while convenient, has led many people to misunderstand thatParse
is a prerequisite toInvoke
. TheInvoke
methods internally callParse
. It seems helpful to separate these.Reduce the number of overloads associated with setting up handlers.
By associating command line options and arguments directly with handler method parameters, the existing API makes it looks like you need one handler parameter for each command line parameter. For example, a command line like
--one 1 --two 2 --three 3
should map to a handler method such asDoSomething(int one, int two, int three)
. This led to a huge number of overloads accepting from 1 to 8 parameters with variations returningvoid
,int
,Task
, orTask<int>
. It's confusing, especially when you need more than 8 parameters. (The answer we provided, usingBinderBase<T>
, isn't discoverable or intuitive.)We'll be pulling out most of these overloads in favor of just the ones that accept
ParseResult
(replacingInvocationContext
in the handler APIs). The result is that the API usage will be the same whether you need one parameter or fifty. In combination with slightly reducing verbosity by reintroducing name-based lookups, the API should be simpler to use. Here’s a comparison:Current:
Proposed:
Provide better support for building source generators.
Working on a source generator for System.CommandLine exposed a number of places where the current configuration API is difficult to use. The middleware pipeline, while flexible, is also not inspectable, making it hard for API users to see what's already been configured. Configurations of unrelated subsystems are unnecessarily coupled via the
CommandLineBuilder
.We plan to make the
CommandLineConfiguration
class mutable and hopefully remove theCommandLineBuilder
altogether (since the motivation for it was to support building immutable configuration objects). (See below for how behaviors will be configurable under this proposal.)Broaden the concept of which symbol types support invocation to include
Option
andDirective
symbols.Commands have always supported invocation. They are the most common indicators of different command line behaviors, while options usually represent parameters to those behaviors. But that's not always the case. Sometimes options have behaviors. The help option (
-h
) is the most familiar example. Then there are directives, a System.CommandLine concept, which also have behaviors that can override the parsed command. We realized that allowing other symbol types to have behaviors, and using a common abstraction for all of these, is clearer than having to implement the option and directive behaviors through middleware.The result is that in the proposed API, a
CliAction
can be attached to any of these types.Remove execution concepts from configuration.
The
CommandLineConfiguration
type actually configures two different aspects of your CLI app:the parser rules that define the CLI's syntax and grammar (e.g.
Command
,Option<T>
,Argument<T>
, POSIX rules, etc), andthe behaviors of the CLI (exception handling, help formats, completion behaviors, typo correction rules).
This proposal would remove the latter entirely from
CommandLineConfiguration
. Behavioral configuration would be available on specificCliAction
-derived types.Make configuration of CLI actions lazy and make associated configuration APIs more cohesive with the handler code.
A common style of CLI app performs one of a set of mutually exclusive actions (subcommands or verbs) and then exits. We've recommended configuring the behaviors of those actions lazily because, since most won't be needed most of the time, it will generally give better startup performance. (We've often made the recommendation not to configure DI globally for CLI apps for this same reason.)
The
CommandLineBuilder
API, though, lends itself to configuring all of these behaviors centrally. This reduces the cohesiveness of the code associated with a specific behavior.Help is a good example to illustrate this.
In the current help API, you might do something like this using the
CommandLineBuilder
andHelpBuilder
to add some text after the default help output:Under this proposal, the code would now look like this:
Other built-in
CliAction
types for existing behavior might includeParseErrorAction
,CompletionAction
, andDisplayVersionAction
. Commands could opt to use custom action types or use a defaultCommandAction
.Replacing existing functionality outright with custom implementations would also be much simpler under the proposed API:
This approach removes abstractions and reduces the concept count relative to the existing builder pattern. The use of pattern matching allows a clearer and more cohesive way to intercept, augment, or replace existing functionality.
Make "pass-through" middleware behaviors simple.
In today's API, command handlers are sometimes overridden at runtime. The
--help
and--version
options do this, as do certain directives like[suggest]
and[parse]
. This is implemented in the middleware pipeline. The middleware checks the parse result for the presence of a given symbol and if it's found, it performs some action and then short circuits the command handler by not calling the pipeline continuation. This is flexible but hard to understand.The
CliAction
design simplifies things by making these kinds of option actions peers to command actions, and then allowing interception to happen via pattern matching (a familiar language construct) rather than middleware (an often unfamiliar abstraction with a special delegate signature). A default order of precedence will be used if you simply callparseResult.Action.RunAsync()
, but by optionally expanding the differentCliAction
types in aswitch
, you can change the order of precedence by just reordering thecase
statements. Or you can useif
patterns. The code flow uses language concepts and is no longer hidden in the middleware.There's one additional middleware use case that this doesn't cover in an obvious way, though, so I'll provide an illustration. This is the case when a middleware performs some action and then calls the continuation delegate. This enables behaviors that happen before and/or after the designated command's behavior.
Under this proposal, you would simply call the other action directly.
Additional implications
ICommandHandler
will be replaced byCliAction
, since the concepts are largely redundant and the nameICommandHandler
won't make sense when applied to options or directives.There will no longer be middleware APIs associated with
CommandLineConfiguration
.The text was updated successfully, but these errors were encountered: