Closed KathleenDollard closed 7 months ago
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.
Simple sample This is a CLI with a single root command and a single option which has an argument:
There is no reason for a simple apps like this one to be async. It just adds unnecessary overhead.
Complex sample This sample has multiple commands with numerous options. The first option does custom validation
It may be a good idea to show the alternative pattern for more than 16 options that the generic overload does not handle. I have run into 16 options limit quite often in the apps that I have tried to port to System.CommandLine.
I would recommend the generic SetHandler
overload for up to say 5 options. It is better to use the alternative pattern beyond that - easier to maintain than a method with very large number of arguments.
Would you recommend reducing the number of overloads then? This could make it easier to find the synchronous versus asynchronous variations within the list.
Would you recommend reducing the number of overloads then?
Yes. I think having the generic overloads for up to 8 options would be enough.
Can we add braces to the proposal and newlines between types. The proposal is fairly difficult to follow and see what starts and ends where.
Normalizing to keywords (System.String
to string
, etc) would also help with the review
Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.
This likely deserves an initial glance over (pre-API review) from @dotnet/area-system-console.
@jeffhandley, @ericstj. This likely deserves its own area as well given that its fairly expansive, in its own repo, and has been driven externally to the libraries team up until this point.
@KathleenDollard is the expectation that the SDK/CLI team is the primary owners of this moving forward?
@terrajobst, this one is marked blocking but likely needs its own API review slot as I expect it will take the full 2 hours due to size.
I know. I asked them to file this issue. It's already scheduled for tomorrow.
There is no reason for a simple apps like this one to be async. It just adds unnecessary overhead.
Are you saying we don't need async overloads or that the sample doesn't need to be async? Commands absolutely need to support asynchrony. I have a bunch of command line tools that make http requests in individual commands.
I am saying that the simple sample does not need to be async.
I do not have a problem with async commands. We have introduced async Main shortcut some years ago and so it makes sense to have async Command shortcut in this library too, for similar reasons.
Just checking 😄
Tagging subscribers to this area: @dotnet/area-system-console See info in area-owners.md if you want to be subscribed.
I've removed blocking from this for the time being so that it doesn't keep showing up in API review. Please re-add the tag when it's time to bring it back. Thanks! :)
Can we add ReadOnlySpan-APIs (or ReadOnlySequence?)
public static class CommandExtensions
{
public static int Invoke(this Command command, ReadOnlySpan<string> args, IConsole console = null);
public static int Invoke(this Command command, ReadOnlySpan<char> commandLine, IConsole console = null);
public static Task<int> InvokeAsync(this Command command, ReadOnlySpan<string> args, IConsole console = null);
public static Task<int> InvokeAsync(this Command command, ReadOnlySpan<char> commandLine, IConsole console = null);
public static ParseResult Parse(this Command command, ReadOnlySpan<string> args);
public static ParseResult Parse(this Command command, ReadOnlySpan<char> commandLine);
}
& Why is Token
not a (readonly) struct?
Why is Token not a (readonly) struct?
To minimise JIT compilation time during startup, according to https://github.com/dotnet/command-line-api/pull/1654.
Can we add ReadOnlySpan-APIs (or ReadOnlySequence?)
It would be de-optimization. Command line parsing should be optimized for minimal code size and startup time. Span APIs like this do not help with that.
Today I tried to use System.CommandLine
for the first time for my quick build script in C# and it was not exactly a pleasant experience I hoped it to be, honestly, it looks extremely verbose to me 😢
var pythonOpt = new Option<string>(
name: "-python",
description: "path to python",
getDefaultValue: () => "python");
var spmiPathOpt = new Option<string>(
name: "-superpmi_path",
description: "path to superpmi.py");
var mchPathOpt = new Option<string>(
name: "-output_mch_path",
description: "output mch path");
var logFileOpt = new Option<string>(
name: "-log_file",
description: "path to log file");
var workDirOpt = new Option<string>(
name: "-benchmarks_path",
description: "path to aspnet benchmarks");
var corerunPathOpt = new Option<string>(
name: "-corerun_path",
description: "path to test corerun");
var dotnetCliPathOpt = new Option<string>(
name: "-dotnetcli_path",
description: "path to dotnet cli to build benchmarks",
getDefaultValue: () => "dotnet");
var tfmOpt = new Option<string>(
name: "-tfm",
description: "target framework",
getDefaultValue: () => "net7.0");
var ridOpt = new Option<string>(
name: "-rid",
description: "path to python",
getDefaultValue: () => "win-x64");
var rootCommand = new RootCommand("TechEmpowerSpmiCollection");
rootCommand.Add(pythonOpt);
rootCommand.Add(spmiPathOpt);
rootCommand.Add(mchPathOpt);
rootCommand.Add(logFileOpt);
rootCommand.Add(workDirOpt);
rootCommand.Add(corerunPathOpt);
rootCommand.Add(dotnetCliPathOpt);
rootCommand.Add(tfmOpt);
rootCommand.Add(ridOpt);
// This doesn't compile -- only up to 9 arguments
/*rootCommand.SetHandler((python, superpmi_path, output_mch_path, log_file, benchmarks_path, corerun_path, dotnetcli_path, tfm_path, rid_opt) =>
{
//
// Do work
//
}, pythonOpt, spmiPathOpt, mchPathOpt, logFileOpt, workDirOpt, corerunPathOpt, dotnetCliPathOpt, tfmOpt, ridOpt);
*/
rooCmd.SetHandler(ctx =>
{
string python = ctx.ParseResult.GetValueForOption(pythonOpt);
string spmiPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string mchPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string rid = ctx.ParseResult.GetValueForOption(pythonOpt);
string tfm = ctx.ParseResult.GetValueForOption(pythonOpt);
string dotnetCliPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string benchmarksPath = ctx.ParseResult.GetValueForOption(benchmarksPathOpt);
string coreRunPath = ctx.ParseResult.GetValueForOption(corerunPathOpt);
// do work
});
return rootCommand.Invoke(args);
and it didn't even compile for me because SetHandler
only supports up to 9 arguments. I understand the intention to build CommandLine
over complicated scenarios first because it's used in our tooling (e.g. dotnet/diagnostics), but it's very difficult to sell this feature for quick build scripts over e.g. https://github.com/commandlineparser/commandline where the same logic for my case would look way less verbose 🙁
This is the same example done with https://github.com/Cysharp/ConsoleAppFramework with AspNetCore minimal api style. Note that it also supports Controller-like command binding
static void Egor(
[Option(null, "path to superpmi.py")] string superpmi_path,
[Option(null, "output mch path")] string output_mch_path,
[Option(null, "path to log file")] string log_file,
[Option(null, "path to aspnet benchmarks")] string benchmarks_path,
[Option(null, "path to test corerun")] string corerun_path,
[Option(null, "path to python")] string python = "python",
[Option(null, "path to dotnet cli to build benchmarks")] string dotnetcli_path = "dotnet",
[Option(null, "target framework")] string tfm = "net7.0",
[Option(null, "runtime identifier")] string rid = "win-x64")
{
// Do work
};
var app = ConsoleApp.Create(args);
app.AddCommand("egor", Egor);
app.Run();
@EgorBo did you try out the System.CommandLine.DragonFruit library? That might give you more what you want.
Also, for an API improvement here: I know it would be untyped and would have to be resolved with reflection, but now that lambdas have implicit conversions to System.Delegate
, there could be an overload of AddHandler(System.Delegate, params IValueDescriptor[])
that would resolve the parameter <-> option mapping at runtime. This overload would allow any number of parameters, though would be slightly less safe at compile time. Since this would only be used for cases where there are more than 9 options, I think that's not the worst situation.
My experience with trying to use the current System.CommandLine was very similar to Egor's.
an API improvement here:
Pure API approach cannot compete with usability of DSLs implemented by the other command line parsing packages mentioned in previous replies. The problem with the proposed API is that it is trying to be both lean and usable, and failing on both. I think that it would be best to make the usability of the direct API use a non-goal, and redesign the API to be a target for the source generator that delivers the desired lean command line parser with good usability.
Also, for an API improvement here: I know it would be untyped and would have to be resolved with reflection, but now that lambdas have implicit conversions to System.Delegate, there could be an overload of AddHandler(System.Delegate, params IValueDescriptor[]) that would resolve the parameter <-> option mapping at runtime. This overload would allow any number of parameters, though would be slightly less safe at compile time. Since this would only be used for cases where there are more than 9 options, I think that's not the worst situation.
@jkoritzinsky, this would reintroduce the need for runtime reflection which was removed for performance and trimmability. But as @jkotas suggested, a layer on top of the core API using source generators could resolve this, and we're indeed working on that.
Currently, the simplest way to go beyond 8 parameters (and arguably a more consistent approach generally) is what @EgorBo used above:
rooCmd.SetHandler(ctx =>
{
string python = ctx.ParseResult.GetValueForOption(pythonOpt);
string spmiPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string mchPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string rid = ctx.ParseResult.GetValueForOption(pythonOpt);
string tfm = ctx.ParseResult.GetValueForOption(pythonOpt);
string dotnetCliPath = ctx.ParseResult.GetValueForOption(pythonOpt);
string benchmarksPath = ctx.ParseResult.GetValueForOption(benchmarksPathOpt);
string coreRunPath = ctx.ParseResult.GetValueForOption(corerunPathOpt);
// do work
});
A less verbose version of this (proposed by @KathleenDollard in a discussion here) might look something like this:
rooCmd.SetHandler(ctx =>
{
string python = ctx.GetValue(pythonOpt);
string spmiPath = ctx.GetValue(pythonOpt);
string mchPath = ctx.GetValue(pythonOpt);
string rid = ctx.GetValue(pythonOpt);
string tfm = ctx.GetValue(pythonOpt);
string dotnetCliPath = ctx.GetValue(pythonOpt);
string benchmarksPath = ctx.GetValue(benchmarksPathOpt);
string coreRunPath = ctx.GetValue(corerunPathOpt);
// do work
});
It’s great we removed the reflection but the API is painful to use. Do we have an issue with what the source generator experience will look like?
The specific issue for InvocationContext.GetValue is https://github.com/dotnet/command-line-api/issues/1785
@davidfowl @jkotas @tannergooding
I put my initial thoughts here: https://github.com/dotnet/command-line-api/issues/1829
Can we have API methods that allow getting a result for argument/option without having to pass the exact instance, but only a name?
For example like these:
public object GetValueForArgument(string argumentName);
public T GetValueForArgument<T>(string argumentName);
public object GetValueForOption(string optionName);
public T GetValueForOption<T>(string optionName);
This would be especially useful if you have a handler that implements the ICommandHandler
interface from System.CommandLine.Invocation
and you register it with UseCommandHandler
from System.CommandLine.Hosting
public sealed class SampleCommandHandler : ICommandHandler
{
public int Invoke(InvocationContext context)
{
throw new NotImplementedException();
}
public async Task<int> InvokeAsync(InvocationContext context)
{
var optionValue = context.ParseResult.GetValueForOption("--option1");
return 0;
}
}
@wiktor-golonka The API allowed this in earlier previews. The problem was that it's much slower (because options can be looked up by several potential aliases and the lookup needs to traverse the command tree in case the option was declared on a parent command, e.g. accounting for myapp --option verb
, myapp -o verb
, myapp verb --option
, etc.)
Another concern with the name-based lookup was that it's potentially ambiguous. It's possible to have different options or commands that have the same name. The object-based lookup allows these to be unambiguous whereas a name-based lookup would require the user to disambiguate them.
Is Program.Main
or top-level statements the primary target for these APIs? It seems like the former. ASP.NET Core seems to be focused primarily on the latter. It would be great to see a "minimal APIs" approach for command-line, which would align our efforts and offerings. Both domains are a kind of routing on text input and controller problem space. Yes?
I'd like to note that minimal apis work good no matter what approach (main/top level) is chosen. @richlander please check out ConsoleAppFramework: https://github.com/Cysharp/ConsoleAppFramework. I've provided an example above (https://github.com/dotnet/runtime/issues/68578#issuecomment-1224321092).
I guess the problem with this approach is that it's quite reflection heavy I guess.
Following proposal is not ready yet, it needs an edit and approval from @KathleenDollard and @jonsequitur.
System.CommandLine has a very ambitious goal. We want to provide a set of simple, intuitive, and performant APIs for parsing of command line arguments, but at the same time we want them to be flexible enough to meet the requirements of users who build complex CLIs like “dotnet”. We basically want to avoid situations where people develop their own parsers because S.CL is missing something important.
The APIs that we want to include in BCL can be grouped into following categories:
We expect these APIs to be building blocks for:
// An example of how incoming source-generator based solution might look like
class Program
{
static int Main(string target, int count = 4)
{
// arguments are already parsed!
}
}
// The alternative
static int Main(string[] args)
{
Command pingCommand = new ("ping")
{
new Argument<string>("target");
new Option<int?>("--count")
};
ParseResult parseResult = pingCommand.Parse(args);
string target = parseResult.GetValue<string>("target")
int count = parseResult.GetValue<int?>("--count") ?? 4;
}
/// > --opt 1 2 3
///
/// The following is equivalent and is always valid:
///
/// > --opt 1 --opt 2 --opt 3
///
/// Building dotnet build
with S.CL:
Implementing ping utility:
Please keep in mind that the example used above does not use the concept of handler/action that we are going to present in a separate proposal.
CliOption
an option, we follow POSIX naming convention used by vast majority of the OSS parsers. Some users might think that it's an optional argument, while it's just a named argument. We want to stick with the existing naming convention, despite the fact that it's not perfect.CliArgument<T>
and CliOption<T>
do not constrain T
. We have considered using IParsable
, but it would require introducing dedicated types for collections of IParsable
, moreover it would limit us to .NET 7.0+. We want to keep supporting .NET Standard 2.0, as plenty of our users are still on .NET Framework.AcceptLegalFilePathsOnly
and AcceptLegalFileNamesOnly
are not constrained to any specific T
, while they make sense only for string
, IEnumerable<string>
Splits a string into a sequence of strings based on whitespace and quotation marks. public static IEnumerable
SplitCommandLine(string commandLine);
What are the parsing rules that this is going to follow? Is the same set of parsing rules as https://github.com/dotnet/runtime/blob/732ae12c9cdd8227ab2882f0949d798c3fcb446d/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs#L225-L226 or different set of rules?
What are the parsing rules that this is going to follow? Is the same set of parsing rules as
It's very close to that and could potentially be made identical. The use case is a little different. This isn't used in the CLI execution code path (since Main
receives an already-split string[]
). This method is used in the code completion code path and is also very valuable for testing (since when people have to do their own split in tests, they very frequently get it wrong.) For this reason, the goal is to conform to people's experience at the shell, which includes an additional layer of escaping.
There's more detailed discussion of this here: https://github.com/dotnet/command-line-api/issues/1758.
AcceptLegalFilePathsOnly and AcceptLegalFileNamesOnly are not constrained to any specific T, while they make sense only for string, IEnumerable
These also work for FileInfo
, DirectoryInfo
, and FileSystemInfo
, and types implementing IEnumerable<T>
for those types. Handling these types is fundamental to many CLIs.
I am surprised that there is no mention of the PowerShell experience, which has had this capability for many years. It uses attributes to describe parameters, their hierarchy and argument validations. It also allows custom completers for external applications.
The use of attributes is quite handy for simple cases, but very verbose for complex ones. That's why I've been voicing the idea for a long time that PowerShell needs a simple parameter description notation that can be translated into attributes. As is this project, too. Why not take a general approach? For example, the following is easy to understand and can be parsed and translated into attributes
> gcm Get-Date -Syntax
Get-Date [[-Date] <datetime>] [-Year <int>] [-Month <int>] [-Day <int>] [-Hour <int>] [-Minute <int>] [-Second <int>] [-Millisecond <int>] [-DisplayHint <DisplayHintType>] [-Format <string>] [-AsUTC] [<CommonParameters>]
Get-Date [[-Date] <datetime>] -UFormat <string> [-Year <int>] [-Month <int>] [-Day <int>] [-Hour <int>] [-Minute <int>] [-Second <int>] [-Millisecond <int>] [-DisplayHint <DisplayHintType>] [-AsUTC] [<CommonParameters>]
Get-Date -UnixTimeSeconds <long> -UFormat <string> [-Year <int>] [-Month <int>] [-Day <int>] [-Hour <int>] [-Minute <int>] [-Second <int>] [-Millisecond <int>] [-DisplayHint <DisplayHintType>] [-AsUTC] [<CommonParameters>]
Get-Date -UnixTimeSeconds <long> [-Year <int>] [-Month <int>] [-Day <int>] [-Hour <int>] [-Minute <int>] [-Second <int>] [-Millisecond <int>] [-DisplayHint <DisplayHintType>] [-Format <string>] [-AsUTC] [<CommonParameters>]
Of course it's not ideal and we should have come up with something better.
Why does PowerShell allow custom completers? While all cmdlets (and .Net assemblies) contain meta-information and PowerShell uses reflection to benefit from it, regular (native C/C++) utilities have no such information at all.
So if this project is attribute-based, then applications based on it could easily be integrated into PowerShell, which could simply read meta-information from application assemblies as it does for cmdlets.
If we look at other shells, such as bash, they prefer custom completers written in bash itself. It is hardly possible to change this (for integration with this project). However, it would be possible to create a universal, simple and fast wrapper that would do the same thing as PowerShell for all applications based on this project and called with the standard complete
command from bash.
Some notes on the above APIs.
CliSymbol.IsHidden
IsHiddenFromHelp
ArgumentArity.ExactlyOne
One
have an "Exactly" prefix? Could it just be One
?ArgumentArity
public ArgumentArity(numberOfValues) : new(numberOfValues, numberOfValues) {}
?CliArgument<T>.DefaultValueFactory
and CliArgument<T>.CustomParser
CliArgument<T>.AcceptOnlyFromAmong
, AcceptLegalFilePathsOnly
, and AcceptLegalFileNamesOnly
Validators
?AcceptOnlyFromAmong
DataAnnotations.AllowedValuesAttribute
AllowedValues
nameCliCommand.Add(CliSymbol symbol)
CliCommand.TreatUnmatchedTokensAsErrors
IgnoreUnmatchedTokens
CliArgument.HelpName
and CliOption.HelpName
I've reviewed through CliCommand
and will resume my review starting at CompletionItem
later.
@iSazonov
I am surprised that there is no mention of the PowerShell experience, which has had this capability for many years.
What you're describing makes sense, though at an API layer above System.CommandLine. Attributes are a common API preference and the System.CommandLine source generator will use them. They still need to drive some lower-level API that handles the core parsing. That lower-level API is System.CommandLine. Once it's done, then integration with PowerShell as you describe, as well as other opinionated APIs, become easier.
If we look at other shells, such as bash, they prefer custom completers written in bash itself. It is hardly possible to change this (for integration with this project). However, it would be possible to create a universal, simple and fast wrapper that would do the same thing as PowerShell for all applications based on this project and called with the standard complete command from bash.
System.CommandLine has completion shim scripts for PowerShell, zsh, and bash. In the long run, generating those scripts, rather than using a shim script that forwards completion requests to another process, will provide some performance benefits, but the lack of common conventions makes it a lot of work. A universal wrapper would be very useful and could emerge from this work.
@jeffhandley
CliSymbol.IsHidden
- Is there a concept of terse and verbose help? Should there be an enum for help inclusion?
It's something we've discussed. The conclusion has generally been that abstractions for help will never be able to handle all of the variety people want, and that a templating language at a higher layer (e.g. one that on detects terminal or even non-terminal UI capabilities and emits VT codes, Markdown, etc.) might be more suitable.
ArgumentArity
- Is it worthwhile to add a ctor for
public ArgumentArity(numberOfValues) : new(numberOfValues, numberOfValues) {}
?
In the large majority of cases, arity is inferred from the generic type parameter, so we kept it out of the constructor for simplicity.
CliArgument<T>.DefaultValueFactory
andCliArgument<T>.CustomParser
- Was there ever an attempt to merge these two such that when no parse input is provided, the parser is called with a null argument result, thus giving the parser an opportunity to return the default value?
The previous iterations did exactly that, but it's akward to use:
new Argument<T>(
parse: argResult => { /* custom parsing logic*/ },
isDefault: /* call the above delegate even if no tokens matched this symbol during parsing */
);
I think we can still improve the ergonomics here.
CliArgument<T>.AcceptOnlyFromAmong
,AcceptLegalFilePathsOnly
, andAcceptLegalFileNamesOnly
- These three validators seem to be getting special treatment, which is odd
- Why are these three promoted to be baked in vs. being validators that can be easily added to
Validators
?
The reasoning is that the need for these is incredibly common in CLI apps. Letting people implement them themselves has a few gotchas for cross-plat as well.
CliCommand.TreatUnmatchedTokensAsErrors
- Consider inverting this with a name change to
IgnoreUnmatchedTokens
This is interesting. I could read "ignore" as meaning I won't be able to see them in the parse result, which isn't the case. For a little more context, the use case for this is sort of like a wildcard. You're expecting tokens that the parser has no rules about and you want to handle them at a later phase, often with a separate parser instance. This is how dotnet new
handles template-specific parsing, for example.
Consider IsHiddenFromHelp
IsHidden affects completion too, so IsHiddenFromHelp would be too restrictive.
What you're describing makes sense, though at an API layer above System.CommandLine.
System.CommandLine layer is essentially implementation details or engine. In fact, we need a user-friendly (and formally strong) language to describe a set of parameters that translates into attributes that is low level language. Both of these levels should be public. The engine itself (and translator/SG) is essentially implementation details and it doesn't require to expose a public API.
I would suggest contacting the PowerShell developers who designed and implemented this mechanism years ago. I haven't had much contact with them on this subject, but it was enough to appreciate what a great job they did in both analysis and implementation.
I'm surprised this project didn't take PowerShell as a starting point. PowerShell is a unique hybrid application that acts as both a shell and a script engine while also being a natural and inseparable extension of .Net - everything written in .Net is natively embedded in PowerShell. So I would expect this project to evolve in that direction. Once again, in this case - all .Net applications will automatically be embedded in PowerShell. Isn't that what Azure's gigantic infrastructure needs?
For native applications, we could create a translator into native code and at the same time generate a pseudo assembly that PowerShell could also use to get the same meta-information. All we would have to do is create a simple, lightweight handler in the native language that would be called from bash (and others), load this pseudo assembly, and do what PowerShell does.
I would suggest contacting the PowerShell developers ... I'm surprised this project didn't take PowerShell as a starting point.
We've been talking with the PowerShell team from the outset of this project. The goals are different enough that converging solutions isn't likely to be useful.
A crucial point is that System.CommandLine is meant to handle the superset of GNU/POSIX and Windows command line grammars, including support for subcommands. PowerShell is stricter and it can't accommodate these less formalized grammars, which predate PowerShell and are more widely used.
So System.CommandLine logically sits lower down in the stack than PowerShell and is less opinionated about its internal API than about its ability to create unopinionated external API (i.e. your CLI). The System.CommandLine API's ergonomic goal is to be as simple as possible. Different API preferences can be met with an additional layer whether attributes, fluent APIs, or DSLs.
I'm not saying that PowerShell should migrate to System.CommandLine. This is not possible and is not necessary. I'm saying that PowerShell needs meta information that allows it to transparently integrate any utility/application and provide at least tab-completion.
Of course we need at least three layers - a parameter description language, an intermediate language (e.g. attributes), a low-level layer. If this project is only the latter and has nothing to do with the rest, then unfortunately it is useless for PowerShell. In general, its usefulness is questionable, because history shows that usually any attempt to create a superset ends up in oblivion. The reason is trivial: a project is not part of the existing ecosystems. If this project does not consider integration with existing ecosystems, the same fate awaits it. If it will be used even in a hundred utilities, this is nothing compared to tens of thousands of utilities in Unix and Windows.
Generating meta information at build time would not allow completion for things like the MSBuild -target:<targets>
option, where the available targets depend on the project file and imported SDKs; or -logger:<logger>
, where the supported logger options depend on the logger type. For these, one still needs a protocol that queries completions from application-specific code at run time, like dotnet-suggest
does. The application-specific completion code can be written separately for each shell as is traditionally done, but it is more convenient for developers of applications and for users of rare shells if the same implementation can be used for all shells.
If the dynamic substitution of arguments requires a full start of the application itself, it is obviously not feasible in the general case. However, PowerShell, for example, sometimes does clever things based on the current context and implementation of the attributes themselves.
If the dynamic substitution of arguments requires a full start of the application itself, it is obviously not feasible in the general case.
It currently does start your process, and performance is acceptable (at least, better than not having completions) but it can definitely be improved. It eventually won't need to start your process, as we'll emit shell-specific completion scripts based on your app's parser. Some shells (zsh, PowerShell) have the ability for completion scripts to call back for dynamic arguments, which the [suggest]
directive will still support on an as-needed basis.
Can someone give me a summary on where this proposal is at?
I saw a few weeks / months back that it was in the API review meetings but it looks like it got cut short due to more important API's taking priority, and thus never went back to get it approved. I now see there's a new proposal? I played around with the old previews of System.CommandLine
and I've even adopted it into one of my personal projects. What's changed in the new proposal and is there a preview build to play around with the new changes?
@KieranDevvs, here are some threads that help summarize the portions currently being worked on:
We're hoping to have a new preview release incorporating these changes in the coming weeks.
System.CommandLine has a very ambitious goal. We want to provide a set of simple, intuitive, and performant APIs for parsing of command line arguments, but at the same time we want them to be flexible enough to meet the requirements of users who build complex CLIs like “dotnet”. We basically want to avoid situations where people develop their own parsers because S.CL is missing something important.
The APIs that we want to include in BCL can be grouped into following categories:
We expect these APIs to be building blocks for:
// An example of how incoming source-generator based solution might look like
class Program
{
static int Main(string target, int count = 4)
{
// arguments are already parsed!
}
}
// The alternative
static int Main(string[] args)
{
Command pingCommand = new ("ping")
{
new Argument<string>("target");
new Option<int?>("--count")
};
ParseResult parseResult = pingCommand.Parse(args);
string target = parseResult.GetValue<string>("target")
int count = parseResult.GetValue<int?>("--count") ?? 4;
}
/// > --opt 1 2 3
///
/// The following is equivalent and is always valid:
///
/// > --opt 1 --opt 2 --opt 3
///
///
/// > myapp -a -b -c
/// > myapp -abc
///
///
/// If an argument is provided after an option bundle, it applies to the last option in the bundle. When
/// > myapp -a -b -c arg
/// > myapp -abc arg
/// > myapp -abcarg
///
///
/// @
can be replaced with zero or more other tokens. This is mostly commonly used to expand tokens from response files and interpolate them into a command line prior to parsing.
/// TBA
Updated version: https://github.com/dotnet/runtime/issues/68578#issuecomment-1490627545
Old version:
Previous version
This output is from our unit test for ensuring PRs do not change the API surface area unexpectedly, and thus is in a slightly non-standard format: ```csharp namespace System.CommandLine { public abstract class Argument : Symbol, IValueDescriptor, ICompletionSource { public ArgumentArity Arity { get; set; } public CompletionSourceList Completions { get; } public bool HasDefaultValue { get; } public string HelpName { get; set; } public Type ValueType { get; } public void AddValidator(ValidateSymbolResult