dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

[API proposal]: System.CommandLine APIs #68578

Closed KathleenDollard closed 7 months ago

KathleenDollard commented 2 years ago

Updated version: https://github.com/dotnet/runtime/issues/68578#issuecomment-1490627545

Old version:

## Background and motivation System.CommandLine is finalizing GA and is thus looking for a final design review prior to V1. There have been significant changes since the previous design review 2 years ago, driven by API review feedback, user feedback, and especially improvements in performance and to enable trimming. Since the surface area is large, we suggest focusing on these questions: ### Namespace reorganization Following the previous API review we segmented the namespaces to specific areas in order to surface the most important types in the root `System.CommandLine` namespace. This resulted in needing a significant number of using statements, as pointed out in the issue [System.CommandLine is split into too many namespaces](https://github.com/dotnet/command-line-api/issues/1680). We'd like to discuss how we balance between too many and too few namespaces. ### Ergonomics Previous feedback included that the coding UX was rather complicated. We have done some simplification, and we have [docs](https://docs.microsoft.com/dotnet/standard/commandline/), but we believe fundamental simplification will require an opinionated layer, probably with source generation for Rolsyn. A community member created a very nice layer for [F# based on computation expressions](https://github.com/JordanMarr/FSharp.SystemCommandLine). Our choice is to keep System.CommandLine as it is and allow these layers to simplify the less complicated use cases. Usability of System.CommandLine has been shown in both static mode (.NET CLI) and dynmaic mode (`dotnet new`). We also have experience with significantly different models with the very popular Dragonfruit, and prototyping subcommands. One of the reasons we want to build a foundation for an opinionated layer is that this is where the community can explore options, including wrappers for current styles used in other parsers. We wanted to check in on thinking around tooling related to API's now that we have source generation as an approach. ### SetHandler One of the things we do to simplify using the API is SetHandler which connects a command with a function/lambda and passes the results for arguments and options. This is effective, and we think is the best approach prior to opinionated layers. However, there are two areas of issues: * Because the delegates are generic and there may be any number of arguments and thus many overloads differing on generic arity, we have 16 overloads of each of the 2 patterns - void returning and task returning. We also document special handling where 16 arguments and options are not enough. * As seen in the [Simple sample](#simple-sample), there is redundancy to pass the option to the SetHandler method. The main mechanism was previously name based, and it was the major source of user issues. We have moved name-based matching into a separate package so it is available for backwards compatibility. Prototyping has confirmed that opinionated layers do not need SetHandler, so we think these issues are ugly, but that this will be an important secondary mechanism for complex CLI's and we do not see a way to avoid it. ### IConsole For testing and other issues we needed an abstraction for Console. We created an interface named IConsole. We strongly hope that .NET will have a future abstraction in lieu of or working with System.Console and hope to avoid a naming collision. At the last API review, our feedback was that an abstraction was very, very unlikely to be an interface, and we wanted to check that was still the case. ### TestConsole We have a buffered console that is used in our testing and we think anyone testing their console output will find valuable. The name indicates more how it is expected to be used than what it is. We think it should be public to help folks, and we think this name is fine since the purpose is the most important thing. ## API Proposal The current state of this proposal can be seen below: https://github.com/dotnet/runtime/issues/68578#issuecomment-1461983285
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 validate); public IEnumerable GetCompletions(CompletionContext context); public object GetDefaultValue(); public void SetDefaultValue(object value); public void SetDefaultValueFactory(Func getDefaultValue); public void SetDefaultValueFactory(Func getDefaultValue); } public class Argument : Argument, IValueDescriptor, IValueDescriptor, ICompletionSource { public Argument(); public Argument(string name, string description = null); public Argument(string name, Func getDefaultValue, string description = null); public Argument(Func getDefaultValue); public Argument(string name, ParseArgument parse, bool isDefault = False, string description = null); public Argument(ParseArgument parse, bool isDefault = False); public Type ValueType { get; } } public struct ArgumentArity : System.ValueType : IEquatable { public ArgumentArity(int minimumNumberOfValues, int maximumNumberOfValues); public static ArgumentArity ExactlyOne { get; } public static ArgumentArity OneOrMore { get; } public static ArgumentArity Zero { get; } public static ArgumentArity ZeroOrMore { get; } public static ArgumentArity ZeroOrOne { get; } public int MaximumNumberOfValues { get; } public int MinimumNumberOfValues { get; } public bool Equals(ArgumentArity other); public bool Equals(object obj); public int GetHashCode(); } public static class ArgumentExtensions { public static TArgument AddCompletions(this TArgument argument, string[] values); public static TArgument AddCompletions(this TArgument argument, Func> complete); public static TArgument AddCompletions(this TArgument argument, CompletionDelegate complete); public static Argument ExistingOnly(this Argument argument); public static Argument ExistingOnly(this Argument argument); public static Argument ExistingOnly(this Argument argument); public static Argument ExistingOnly(this Argument argument); public static TArgument FromAmong(this TArgument argument, string[] values); public static TArgument LegalFileNamesOnly(this TArgument argument); public static TArgument LegalFilePathsOnly(this TArgument argument); public static ParseResult Parse(this Argument argument, string commandLine); public static ParseResult Parse(this Argument argument, string[] args); } public class Command : IdentifierSymbol, IEnumerable, IEnumerable, ICompletionSource { public Command(string name, string description = null); public IReadOnlyList Arguments { get; } public IEnumerable Children { get; } public ICommandHandler Handler { get; set; } public IReadOnlyList