-
Notifications
You must be signed in to change notification settings - Fork 395
Announcing System.CommandLine 2.0 Beta 4 #1750
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
Comments
If before beta 4, I used a lambda with "magic" IConsole parameter (for instance rootCommand.SetHandler(context =>
// Using this line I cannot get to the IConsole console...
// rootCommand.SetHandler((env, module) =>
//
// Before beta 4 I used this line
// rootCommand.SetHandler((Environment? env, Module module, IConsole console) =>
{
var module = context.ParseResult.GetValueForOption(optionModule);
var env = context.ParseResult.GetValueForOption(optionEnvironment);
// blah blah
context.Console.Out.Write(response);
} |
Yes. We wanted to limit the number of overloads. But it should be straightforward to define your own extension methods if you prefer to have the parameters for both injected objects and bound command line values in the same handler signature. |
@jonsequitur So with beta 4, if I want access to the That seems to mean that there no way to use a custom model binder and have access to a cancellation token at the same time. If that is true, then this seems like an odd backstep to me. |
@bording The |
@jonsequitur I think you're missing my point. If I'm using the I'm currently using custom model binders, and injecting an |
Before I was able to specify types on the handler that would automatically injected, and would resolve to the services registered in a middleware on startup. So a Command class would look something like this: using System.CommandLine;
using System.CommandLine.Binding;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using MyApp.Cli.Commands.User;
using MyApp.Cli.Primitives;
using MyApp.Cli.Services;
namespace MyApp.Cli.Commands;
public class UserCommand : Command
{
public UserCommand() : base("user")
{
Description = "Display user authentication status";
AddCommand(new LoginCommand());
AddCommand(new LogoutCommand());
var refreshOption = new Option<bool>("--refresh", "Force access token refresh even if hasn't expired yet");
Add(refreshOption);
this.SetHandler<bool, TokenService, Auth0Client, InvocationContext>(UserHandler, refreshOption);
}
public async Task UserHandler(
bool refresh, TokenService tokenService, Auth0Client auth0Client, InvocationContext context)
{
var console = context.Console;
var cancellationToken = context.GetCancellationToken();
// [...] But after this change I'm not able to easily inject both options/arguments and resolved services. using System.CommandLine.Binding;
using Microsoft.Extensions.DependencyInjection;
namespace MyApp.Cli.Primitives;
public class Injectable<T> : BinderBase<T>
{
protected override T GetBoundValue(BindingContext bindingContext)
{
return (T)bindingContext.GetRequiredService(typeof(T));
}
} That allows me to write the code like this, what looks even cleaner;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using MyApp.Cli.Commands.User;
using MyApp.Cli.Primitives;
using MyApp.Cli.Services;
namespace MyApp.Cli.Commands;
public class UserCommand : Command
{
public UserCommand() : base("user")
{
Description = "Display user authentication status";
AddCommand(new LoginCommand());
AddCommand(new LogoutCommand());
var refreshOption = new Option<bool>("--refresh", "Force access token refresh even if hasn't expired yet");
Add(refreshOption);
this.SetHandler(
UserHandler,
refreshOption,
new Injectable<TokenService>(),
new Injectable<Auth0Client>(),
new Injectable<InvocationContext>());
}
public async Task UserHandler(
bool refresh, TokenService tokenService, Auth0Client auth0Client, InvocationContext context)
{
var console = context.Console;
var cancellationToken = context.GetCancellationToken();
// [...] Maybe this type of Injectable helper can be added to the library? |
Should I use the |
Yes. When you declare options and arguments, you'll need to use You can use foreach (Option opt in command.Options)
{
} |
So what should I do now if I used one of removed overloads? I was passing 11 of |
Now that options are required to be passed to SetHandler (I like this change), how about allowing us to omit also adding them directly to the command. In other words, any option passed to SetHandler is implicitly also added to the root command. |
We've looked into this and while it works for the simple case, there are a number of cases where this API would lead to ambiguities. I outlined a few here: #1537 (comment) |
@tomrus88, you can either use a custom |
Hi folks, I needed the var name = new Option<String>("name", "Your name!");
var ctvs = new CancellationTokenValueSource();
var root = new RootCommand("Root command!") { name };
root.SetHandler(async (name, ct) => {
Console.Write("name: ");
try { await Task.Delay(5000, ct); }
finally { Console.WriteLine(name); }
}, name, ctvs);
var clb = new CommandLineBuilder(root);
clb.CancelOnProcessTermination();
return await clb.Build().InvokeAsync(args); class CancellationTokenValueSource : IValueDescriptor<CancellationToken>, IValueSource
{
public Boolean TryGetValue(IValueDescriptor valueDescriptor, BindingContext bindingContext, out Object? boundValue)
{
boundValue = (CancellationToken)bindingContext.GetService(typeof(CancellationToken))!;
return true;
}
public Object? GetDefaultValue() => throw new NotImplementedException();
public String ValueName => throw new NotImplementedException();
public Type ValueType => throw new NotImplementedException();
public Boolean HasDefaultValue => throw new NotImplementedException();
} Run |
After updating from beta3 to beta4 I have several erros with the following snippet. testCommand.SetHandler<int, bool, FileInfo, bool, IConsole>((intOption, boolOption, fileOption, verbose, console) =>
{
if (verbose)
{
console.Out.Write(nameof(addTestCommand));
console.Out.Write(Environment.NewLine);
}
_logger.Info($"Command used: {testCommand.Name}{Environment.NewLine}" +
$"\t{nameof(intOption)}: \"{intOption}\"{Environment.NewLine}" +
$"\t{nameof(boolOption)}: \"{boolOption}\"{Environment.NewLine}" +
$"\t{nameof(fileOption)}: {fileOption}");
console.Out.Write($"The value for --int-option is: {intOption}");
console.Out.Write(Environment.NewLine);
console.Out.Write($"The value for --bool-option is: {boolOption}");
console.Out.Write(Environment.NewLine);
console.Out.Write($"The value for --file-option is: {fileOption?.FullName ?? "null"}");
console.Out.Write(Environment.NewLine);
if (verbose)
{
console.Out.Write($"Done!");
}
}, optionInt, optionBool, optionFile, optionVerbose);
So this is my updated still not working code: testCommand.SetHandler<int, bool, FileInfo, bool, InvocationContext>(async (intOption, boolOption, fileOption, verbose, context) =>
{
if (verbose)
{
context.Console.Out.Write(nameof(addTestCommand));
context.Console.Out.Write(Environment.NewLine);
}
_logger.Info($"Command used: {testCommand.Name}{Environment.NewLine}" +
$"\t{nameof(intOption)}: \"{intOption}\"{Environment.NewLine}" +
$"\t{nameof(boolOption)}: \"{boolOption}\"{Environment.NewLine}" +
$"\t{nameof(fileOption)}: {fileOption}");
context.Console.Out.Write($"The value for --int-option is: {intOption}");
context.Console.Out.Write(Environment.NewLine);
context.Console.Out.Write($"The value for --bool-option is: {boolOption}");
context.Console.Out.Write(Environment.NewLine);
context.Console.Out.Write($"The value for --file-option is: {fileOption?.FullName ?? "null"}");
context.Console.Out.Write(Environment.NewLine);
if (verbose)
{
context.Console.Out.Write($"Done!");
}
await Task.CompletedTask;
}, optionInt, optionBool, optionFile, optionVerbose); Is this no longer supported or do I use the library wrong? |
@bgever I just realized my solution is almost the same as yours, but mine's worse haha. Thanks for posting it! |
@bgever @NickStrupat I hit on more or less the same solution. @FroggieFrog You could use this same pattern to bind your |
So what "just worked" before, now needs to be done by hand with way more lines of code? |
Hi, |
Hi, |
@FroggieFrog Here's the implementation I'm using. It might be reasonable to add this into System.CommandLine, but in the meantime it's not much code: internal static class Bind
{
public static ServiceProviderBinder<T> FromServiceProvider<T>() => ServiceProviderBinder<T>.Instance;
internal class ServiceProviderBinder<T> : BinderBase<T>
{
public static ServiceProviderBinder<T> Instance { get; } = new();
protected override T GetBoundValue(BindingContext bindingContext) => (T)bindingContext.GetService(typeof(T));
}
} With this, your sample would pass one extra to testCommand.SetHandler((intOption, boolOption, fileOption, verbose, console) => // 👈 parameter types are now inferred
{
// ...
}, optionInt, optionBool, optionFile, optionVerbose, Bind.FromServiceProvider<IConsole>()); // 👈 call service provider binder here |
@jonsequitur
I really hope this will be added. |
Like your solution @jonsequitur! FYI, I'm using the extension method |
I am posting my sample from last time, which this update of SetHandler broke again: internal static class Program
{
private static int Main(string[] args)
{
// Create options for the export command.
Option[] exportOptions = new Option[]
{
new Option<bool>("--reports", "Exports reports."),
new Option<bool>("--files", "Exports files.")
};
// Create options for the check command.
Option[] checkOptions = new Option[]
{
new Option<bool>("--reports", "Checks reports."),
new Option<bool>("--files", "Checks files.")
};
// Create a root command containing the sub commands.
RootCommand rootCommand = new RootCommand
{
new Command("export", "Writes files containing specified export options.").WithHandler<bool, bool>(Program.HandleExport, exportOptions),
new Command("check", "Checks the specified options.").WithHandler<bool, bool>(Program.HandleCheck, checkOptions)
};
return rootCommand.Invoke(args);
}
private static Task<int> HandleExport(bool reports, bool files)
{
return Task.FromResult(0);
}
private static Task<int> HandleCheck(bool reports, bool files)
{
return Task.FromResult(0);
}
}
internal static class Extensions
{
public static Command WithHandler<T1, T2>(this Command command, Func<T1, T2, Task> handler, Option[] options)
{
options.ForEach(option => command.AddOption(option));
command.SetHandler(handler, options);
return command;
}
} You can not be serious that i have to specify the type and position each time i want to add the option. With every release you offload more and more to the user of the library making it more complex to use. internal static class Extensions
{
public static Command WithHandler<T1, T2>(this Command command, Func<T1, T2, Task> handler, Option[] options)
{
options.ForEach(option => command.AddOption(option));
command.SetHandler(handler, (IValueDescriptor<T1>)options[0], (IValueDescriptor<T2>)options[1]);
return command;
}
} |
I scoured through the sample on how to pass arguments to SetHandler is supposed to be used: var delayOption = new Option<int>
("--delay", "An option whose argument is parsed as an int.");
var messageOption = new Option<string>
("--message", "An option whose argument is parsed as a string.");
var rootCommand = new RootCommand("Parameter binding example");
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);
rootCommand.SetHandler(
(delayOptionValue, messageOptionValue) =>
{
DisplayIntAndString(delayOptionValue, messageOptionValue);
},
delayOption, messageOption);
await rootCommand.InvokeAsync(args); This code looks like it is written by someone who is writing code for the first time. There is so much duplication in it that i am wondering who is approving this stuff. |
@Balkoth We'd love to hear any productive suggestions, but the tone of this comment isn't that. Ok, so the decision to not couple the parsing API to the parameter binding API is deliberate. Creating wrapper libraries that make this more concise is trivial but it requires embedding conventions that limit the CLI grammars that can be supported by the parser, so they probably belong at a higher layer. But for a given codebase, if you don't need some of those features, you can be more opinionated. Here's a refactor of your sample: internal static class Program
{
private static int Main(string[] args)
{
// Create a root command containing the sub commands.
RootCommand rootCommand = new RootCommand
{
new Command("export", "Writes files containing specified export options.")
.WithHandler(HandleExport,
new Option<bool>("--reports", "Exports reports."),
new Option<bool>("--files", "Exports files.")),
new Command("check", "Checks the specified options.")
.WithHandler(HandleCheck,
new Option<bool>("--reports", "Checks reports."),
new Option<bool>("--files", "Checks files."))
};
return rootCommand.Invoke(args);
}
private static Task<int> HandleExport(bool reports, bool files)
{
return Task.FromResult(0);
}
private static Task<int> HandleCheck(bool reports, bool files)
{
return Task.FromResult(0);
}
}
internal static class Extensions
{
public static Command WithHandler<T1, T2>(this Command command, Func<T1, T2, Task> handler, IValueDescriptor<T1> symbol1, IValueDescriptor<T2> symbol2)
{
command.Add(symbol1);
command.Add(symbol2);
command.SetHandler(handler, symbol1, symbol2);
return command;
}
} This change to In fact, we've explored making There are problems. It's unclear how global options would work. Here's another. Consider this parser setup: var option = new Option<int>("-i");
var subcommand1 = new Command("subcommand1");
subcommand1.SetHandler(i => {}, option);
var subcommand2 = new Command("subcommand2");
subcommand2.SetHandler(i => {}, option);
var root = new RootCommand
{
option,
subcommand1,
subcommand2
}; The option > myapp -i subcommand1 But as you can see from the Put differently, the parser API defines the grammar, while the Suggestions are welcome for making this more concise without introducing coupling. Our current plan though is to keep them decoupled, improve the ergonomics to the degree possible given that constraint, and allow additional layers (in some cases source generator-driven) to provide more terse and opinionated APIs. |
Imho you keep making weird decisions by offloading more and more development work to me with each release. I used this library so that i don't have to create wrapper libraries. Your changes and justifications for it alienate me so hard, that i may just look for another solution. This is what your changes have done so far. I was excited at first l, that finally a command line parsing library from microsoft is around. But then it turns into such a chore. P.S.: Don't shoot the messger. |
Those of us working on System.CommandLine are also not satisfied yet with this API, which is why there's still a beta tag on it. The changes so far have been to remove ambiguity from the API because both On the subject of wrapper libraries, people will write them because they want their own conventions, but we fully intend to make System.CommandLine work well on its own. |
For the code posted by @jonsequitur ( Using those samples without that step fails:
|
@lonix1 you should register them in the Middleware:
|
@JobaDiniz Of all the places I looked, registering services in middleware didn't even cross my mind. This library's API is really odd. 😄 Thanks for your help! (I actually did register services in |
It was moved into the
We're working on simplified API approaches to layer on top of the core parser, which is intentionally more low-level. This will be shipped in a separate package.
This is definitely unintuitive and another place where an additional API layer will help. The reason it's there is that CLI apps are often very short-lived so we want to do any registration work lazily. For example, during command line completion, System.CommandLine performs parsing but not invocation, so registered services are not used. |
Been considering that... I agree IoC may be overkill since we're invoking just one command. In that case, why use it at all? Why not just define/instantiate services within each command/handler. There are probably use cases I've not encountered, but right now I'm gravitating toward avoiding IoC completely; for simple cli apps I see no benefit other than more abstraction and busywork.
I also want to define commands and/or handlers in separate classes, and found a way to do so. But when the API is redesigned, it would be nice to be able to use lambdas anyway, if we want them. The alternative is do it the way all the other libraries do, which is to use attributes for everything. That makes the code cleaner, but then everything must be compile-time constant. One cannot define names, descriptions, etc., as anything other than const, it's harder to localise, etc. |
Many people asked for this so we simply exposed the internal mechanism that was already in place to support injection of System.CommandLine types. To your point, in the straw man example above,
I expect all of these uses cases to remain intact through any API reworking to come. All three are valid preferences. Currently, the lambda is a shortcut to defining an |
Yes I quickly realised that's the problem here. There are so many stakeholders (many inside MS) and everyone wants something else, according to different design expecations. I don't envy you your job in this, must be tough to keep everyone happy. 😄 I think the lowest common denominator is to design an API that follows the Principle of Least Surprise, and implement weird requirements on top of that. |
IMO I think you should reconsider whether the pattern of It's clear that the handler was originally designed to be dynamic and easy to use, but that conflicts with new requirements to make the library strongly typed and compatible with AOT. Unfortunately Today I had to entirely drop using generic arguments everywhere because I need Maybe |
Thanks @NickStrupat, that worked! But make sure to call |
For cases in which the help document describing all the options is quite long, there is a definite need to be able to control when the full help document is shown and when just an error message about the command-line is shown. Cf. #1214 |
I encountered this issue also, but found a way around it. By looking into the source code a bit for how values for options / arguments are retrieved, I wrote this extension class public static class CliExtensions
{
public static T GetValue<T>(this InvocationContext ctx, IValueDescriptor<T> symbol)
{
if (symbol is IValueSource valueSource &&
valueSource.TryGetValue(symbol, ctx.BindingContext, out var boundValue) &&
boundValue is T value)
{
return value;
}
return symbol switch
{
Argument<T> argument => ctx.ParseResult.GetValueForArgument(argument),
Option<T> option => ctx.ParseResult.GetValueForOption(option),
_ => throw new ArgumentOutOfRangeException()
};
}
} With it, I can just var option1 = new Option<bool>("--option1");
var option2 = new Option<bool>("--option2");
var command = new Command("test"){ option1, option2 };
command.SetHandler((ctx) => {
var value1 = ctx.GetValue(value1);
var value2 = ctx.GetValue(value2);
// Do stuff
}); I suppose I could even just add my own extension methods to Rather odd to have an arbitrary upper-bound on number of options / arguments, especially set at 8. Would have made sense to at least have a separate overload which allowed any number of options but less readable / friendly as with typed overloads - just to ensure cases where more than 8 is needed, are still possible, if not nearly as "neat" looking. |
@micdah, the SetHandler overloads were removed yesterday in #2089. There are now only Command.SetHandler(Action<InvocationContext>) and Command.SetHandler(Func<InvocationContext, CancellationToken, Task>), and the handlers have to query the values from InvocationContext, most conveniently using the InvocationContext.GetValue methods that were added in #1787. InvocationContext itself is also going to be replaced with ParseResult, according to #2046 (comment). |
Same to me. The BindingContext provides the Add Service that we use to register dependencies. But the lack of an IServiceCollection configuration step really difficult my job to register libraries that provides clean and simple extensions methods. |
Is there a plan to update the public docs on |
Sorry if asked before but when can we expect a GA release? |
I think never we have the release of its. :D |
Not ideal :) |
When trying to add the user option ran into error: "No overload for method 'SetHandler' takes 10 arguments" The Docs for SetHandler are wrong!! LINKS: GITHUB ISSUE: dotnet/command-line-api#1750 DOCS: https://learn.microsoft.com/en-us/dotnet/standard/commandline/model-binding#parameter-binding-more-than-8-options-and-arguments
System.CommandLine 2.0 Beta 4
We've been working through feedback on the beta 2 and beta 3 releases of System.CommandLine and now we're ready with beta 4. Here's a quick overview of what's new.
Microsoft Docs
Probably the most important update isn't in the API itself but in the release of preview documentation for System.CommandLine. It's been live now since April and, like the library, is in preview. There's a good deal of API to cover so if there's something that's missing or unclear, please let us know.
SetHandler
simplificationThe most common feedback we've heard about the
SetHandler
API introduced in beta 2 is that it's confusing, mainly because you can pass any number ofIValueDescriptor<T>
instances (e.g.Argument<T>
,Option<T>
, orBinderBase<T>
) to theparams IValueDescriptor[]
parameter on most of the overloads, and this number can differ from the count of the handler's generic parameters.I'll use this simple parser setup in the following examples:
Here's an example of the old
SetHandler
signature:The expectation was that in the common case, you would call this method like this:
But the fact that the final parameter to the old
SetHandler
methods was aparams
array meant that the following would compile and then fail at runtime:The logic behind this method signature was that any handler parameters without a matching
IValueDescriptor
would be satisfied instead by falling back toBindingContext.GetService
. This has been a longstanding way that dependency injection was supported for both System.CommandLine types such asParseResult
as well as for external types. But it's not the common case, and it's confusing.We've changed the
SetHandler
methods to require a singleIValueDescriptor<T>
for each generic parameterT
, rather than the previousparams
array:This has a number of benefits. In addition to making it clearer that you also need to pass the
Option<T>
andArgument<T>
instances corresponding to the generic parameters, it also works with C# type inference to make your code a little more concise. You no longer need to restate the parameter types, because they're inferred:We've also added overloads accepting
InvocationContext
, so if you want more detailed access for DI or larger numbers of options and arguments, you can do this:Finally, we've reduced the number of
SetHandler
overloads. Where before there were overloads supporting up to sixteen generic parameters, they now only go up to eight.Option
andArgument
(non-generic) are nowabstract
Support has existed since the earliest previews of System.CommandLine for creating non-generic
Option
andArgument
instances that could be late-bound to a handler parameter. For example, anArgument
could be created that could later be bound to either astring
or anint
parameter. This relied heavily on the reflection code that poweredCommandHandler.Create
, which was moved out of System.CommandLine and into the System.CommandLine.NamingConventionBinder package in beta 2. Fully removing support for this late-binding behavior by makingOption
andArgument
abstract has helped simplify some code, clarify the API, and provided additional performance benefits.Response file format unification
Response file support is a common feature of a number of command line tools, allowing a token within the command line to be replaced by inlining the contents of a file. You can think of a response file as a sort of configuration file that uses the same grammar as the command line tool itself. For example, given a file called
response_file.rsp
, the following command line will interpolate the contents of that file into the command line as if you had run it directly:> myapp @response_file.rsp
(The
.rsp
file extension is another common convention, though you can use any file extension you like).Until this release, System.CommandLine supported two different formats for response files: line-delimited and space-delimited. Line-delimited response files are more common and thus were the default. If
response_file.rsp
contained the following:then it would be treated as though you had run this:
> myapp --verbosity very
(Note following a
#
, the rest of the text on that line is treated as a comment and the parser will ignore it.)This also provided a mechanism for avoiding the sometimes confusing rules of escaping quotes, which differ from one shell to another. You could produce a single token with a line-delimited response file by putting multiple words on a single line.
If
response_file.rsp
contained the following:then it would be treated as though you had run this:
> myapp --greeting "Good morning!"
System.CommandLine also supported another response file format, space-separated, where the content of the response file was expected to be all on a single line and was interpolated verbatim into the command line. This was not commonly used and we decided it would simplify the developer choice and make the behavior clearer for end users to unify the format.
The rules for the new format are as follows:
Good morning!
will be treated as two tokens,Good
andmorning!
."Good morning!"
), you must enclose them in quotes.#
symbol and the end of the line is treated as a comment and ignored.@
can reference additional response files.Custom token replacement
While we were trying to figure out how to make response file handling simpler, we saw that there are so many variations on how to parse response files that we could never provide a single implementation that could address them all. This could easily become a deal-breaker for people who have older apps that they would like to reimplement using System.CommandLine. So while we were consolidating response file handling, we also decided to generalize and hopefully future-proof the solution by adding a new extensibility point. After all, the core logic of response files is simple: during tokenization, a token beginning with
@
is (possibly) replaced by zero or more other tokens.If the default response file handling doesn't work for you, you can now replace it using the
CommandLineBuilder.UseTokenReplacer
method. The following example configures the parser to replace@
-prefixed tokens with other tokens.This token replacement happens prior to parsing, so just as with response files, the new tokens can be parsed as commands, options, or arguments.
If you return
true
from the token replacer, it will cause the@
-prefixed token to be replaced. If you returnfalse
, it will be passed along to the parser unchanged, and any value you assign to theerrorMessage
parameter will be displayed to the user.Given the above token replacement logic, an app will parse as follows:
It's worth noting that this behavior is recursive, just as with response files. If a replacement token begins with
@
, the token replacer will be called again to potentially replace that token.Added support for NativeAOT compilation
Continuing the work done in beta 3 to make apps built using System.CommandLine trimmable, they can now also be compiled with Native AOT. For more details, you can read about Native AOT compilation in the .NET 7 Preview 3 announcement here: https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-3/#faster-lighter-apps-with-native-aot.
The text was updated successfully, but these errors were encountered: