Skip to content
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

[API proposal]: System.CommandLine APIs #68578

Closed
KathleenDollard opened this issue Apr 26, 2022 · 55 comments
Closed

[API proposal]: System.CommandLine APIs #68578

KathleenDollard opened this issue Apr 26, 2022 · 55 comments
Labels
api-approved API was approved in API review, it can be implemented area-System.Console
Milestone

Comments

@KathleenDollard
Copy link
Contributor

KathleenDollard commented Apr 26, 2022

Updated version: #68578 (comment)

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. 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, 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. 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, 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: #68578 (comment)

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:
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<ArgumentResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public object GetDefaultValue();
        public void SetDefaultValue(object value);
        public void SetDefaultValueFactory(Func<object> getDefaultValue);
        public void SetDefaultValueFactory(Func<ArgumentResult,object> getDefaultValue);
    }

    public class Argument<T> : Argument, IValueDescriptor<T>, IValueDescriptor, ICompletionSource 
    {
        public Argument();
        public Argument(string name, string description = null);
        public Argument(string name, Func<T> getDefaultValue, string description = null);
        public Argument(Func<T> getDefaultValue);
        public Argument(string name, ParseArgument<T> parse, bool isDefault = False, string description = null);
        public Argument(ParseArgument<T> parse, bool isDefault = False);
        public Type ValueType { get; }
    }

    public struct ArgumentArity : System.ValueType : IEquatable<ArgumentArity> 
    {
        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<TArgument>(this TArgument argument, string[] values);
        public static TArgument AddCompletions<TArgument>(this TArgument argument, Func<CompletionContext,IEnumerable<string>> complete);
        public static TArgument AddCompletions<TArgument>(this TArgument argument, CompletionDelegate complete);
        public static Argument<FileInfo> ExistingOnly(this Argument<FileInfo> argument);
        public static Argument<DirectoryInfo> ExistingOnly(this Argument<DirectoryInfo> argument);
        public static Argument<FileSystemInfo> ExistingOnly(this Argument<FileSystemInfo> argument);
        public static Argument<T> ExistingOnly<T>(this Argument<T> argument);
        public static TArgument FromAmong<TArgument>(this TArgument argument, string[] values);
        public static TArgument LegalFileNamesOnly<TArgument>(this TArgument argument);
        public static TArgument LegalFilePathsOnly<TArgument>(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<Symbol>, IEnumerable, ICompletionSource 
    {
        public Command(string name, string description = null);
        public IReadOnlyList<Argument> Arguments { get; }
        public IEnumerable<Symbol> Children { get; }
        public ICommandHandler Handler { get; set; }
        public IReadOnlyList<Option> Options { get; }
        public IReadOnlyList<Command> Subcommands { get; }
        public bool TreatUnmatchedTokensAsErrors { get; set; }
        public void Add(Option option);
        public void Add(Argument argument);
        public void Add(Command command);
        public void AddArgument(Argument argument);
        public void AddCommand(Command command);
        public void AddGlobalOption(Option option);
        public void AddOption(Option option);
        public void AddValidator(ValidateSymbolResult<CommandResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public IEnumerator<Symbol> GetEnumerator();
    }

    public static class CommandExtensions 
    {
        public static int Invoke(this Command command, string[] args, IConsole console = null);
        public static int Invoke(this Command command, string commandLine, IConsole console = null);
        public static Task<int> InvokeAsync(this Command command, string[] args, IConsole console = null);
        public static Task<int> InvokeAsync(this Command command, string commandLine, IConsole console = null);
        public static ParseResult Parse(this Command command, string[] args);
        public static ParseResult Parse(this Command command, string commandLine);
    }

    public class CommandLineConfiguration 
    {
        public CommandLineConfiguration(Command command, bool enablePosixBundling = True, bool enableDirectives = True, bool enableLegacyDoubleDashBehavior = False, bool enableTokenReplacement = True, LocalizationResources resources = null, IReadOnlyList<InvocationMiddleware> middlewarePipeline = null, Func<BindingContext,HelpBuilder> helpBuilderFactory = null, System.CommandLine.Parsing.TryReplaceToken tokenReplacer = null);
        public bool EnableDirectives { get; }
        public bool EnableLegacyDoubleDashBehavior { get; }
        public bool EnablePosixBundling { get; }
        public bool EnableTokenReplacement { get; }
        public LocalizationResources LocalizationResources { get; }
        public Command RootCommand { get; }
        public void ThrowIfInvalid();
    }

    public class CommandLineConfigurationException : Exception, System.Runtime.Serialization.ISerializable 
    {
        public CommandLineConfigurationException(string message);
        public CommandLineConfigurationException();
        public CommandLineConfigurationException(string message, Exception innerException);
    }

    public static class CompletionSourceExtensions 
    {
        public static void Add(this CompletionSourceList completionSources, Func<CompletionContext,IEnumerable<string>> complete);
        public static void Add(this CompletionSourceList completionSources, CompletionDelegate complete);
        public static void Add(this CompletionSourceList completionSources, string[] completions);
    }

    public class CompletionSourceList : IEnumerable<ICompletionSource>, IReadOnlyCollection<ICompletionSource>, IReadOnlyList<ICompletionSource>, IEnumerable 
    {
        public CompletionSourceList();
        public int Count { get; }
        public ICompletionSource Item { get; }
        public void Add(ICompletionSource source);
        public void Clear();
        public IEnumerator<ICompletionSource> GetEnumerator();
    }

    public static class ConsoleExtensions 
    {
        public static void Write(this IConsole console, string value);
        public static void WriteLine(this IConsole console, string value);
    }

    public class DirectiveCollection : IEnumerable<KeyValuePair<string,IEnumerable<string>>>, IEnumerable 
    {
        public DirectiveCollection();
        public bool Contains(string name);
        public IEnumerator<KeyValuePair<string,IEnumerable<string>>> GetEnumerator();
        public bool TryGetValues(string name, ref IReadOnlyList<string> values);
    }

    public static class Handler 
    {
        public static void SetHandler(this Command command, Action handle);
        public static void SetHandler(this Command command, Func<Task> handle);
        public static void SetHandler<T>(this Command command, Action<T> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2>(this Command command, Action<T1,T2> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3>(this Command command, Action<T1,T2,T3> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4>(this Command command, Action<T1,T2,T3,T4> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5>(this Command command, Action<T1,T2,T3,T4,T5> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Action<T1,T2,T3,T4,T5,T6> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T>(this Command command, Func<T,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2>(this Command command, Func<T1,T2,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3>(this Command command, Func<T1,T2,T3,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4>(this Command command, Func<T1,T2,T3,T4,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5>(this Command command, Func<T1,T2,T3,T4,T5,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Func<T1,T2,T3,T4,T5,T6,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,Task> handle, IValueDescriptor[] symbols);
    }

    public interface IConsole : IStandardError, IStandardIn, IStandardOut 
    {
    }

    public abstract class IdentifierSymbol : Symbol, ICompletionSource 
    {
        public IReadOnlyCollection<string> Aliases { get; }
        public string Name { get; set; }
        public void AddAlias(string alias);
        public bool HasAlias(string alias);
    }

    public class LocalizationResources 
    {
        public static LocalizationResources Instance { get; }
        public string ArgumentConversionCannotParse(string value, Type expectedType);
        public string ArgumentConversionCannotParseForCommand(string value, string commandAlias, Type expectedType);
        public string ArgumentConversionCannotParseForOption(string value, string optionAlias, Type expectedType);
        public string DirectoryDoesNotExist(string path);
        public string ErrorReadingResponseFile(string filePath, System.IO.IOException e);
        public string ExceptionHandlerHeader();
        public string ExpectsFewerArguments(Token token, int providedNumberOfValues, int maximumNumberOfValues);
        public string ExpectsOneArgument(SymbolResult symbolResult);
        public string FileDoesNotExist(string filePath);
        public string FileOrDirectoryDoesNotExist(string path);
        protected string GetResourceString(string resourceString, object[] formatArguments);
        public string HelpAdditionalArgumentsDescription();
        public string HelpAdditionalArgumentsTitle();
        public string HelpArgumentDefaultValueLabel();
        public string HelpArgumentsTitle();
        public string HelpCommandsTitle();
        public string HelpDescriptionTitle();
        public string HelpOptionDescription();
        public string HelpOptionsRequiredLabel();
        public string HelpOptionsTitle();
        public string HelpUsageAdditionalArguments();
        public string HelpUsageCommand();
        public string HelpUsageOptions();
        public string HelpUsageTitle();
        public string InvalidCharactersInFileName(System.Char invalidChar);
        public string InvalidCharactersInPath(System.Char invalidChar);
        public string NoArgumentProvided(SymbolResult symbolResult);
        public string RequiredArgumentMissing(SymbolResult symbolResult);
        public string RequiredCommandWasNotProvided();
        public string ResponseFileNotFound(string filePath);
        public string SuggestionsTokenNotMatched(string token);
        public string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCollection<string> allowedValues);
        public string UnrecognizedCommandOrArgument(string arg);
        public string VersionOptionCannotBeCombinedWithOtherArguments(string optionAlias);
        public string VersionOptionDescription();
    }

    public abstract class Option : IdentifierSymbol, IValueDescriptor, ICompletionSource 
    {
        public bool AllowMultipleArgumentsPerToken { get; set; }
        public string ArgumentHelpName { get; set; }
        public ArgumentArity Arity { get; set; }
        public bool IsRequired { get; set; }
        public Type ValueType { get; }
        public void AddValidator(ValidateSymbolResult<OptionResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public bool HasAliasIgnoringPrefix(string alias);
        public void SetDefaultValue(object value);
        public void SetDefaultValueFactory(Func<object> getDefaultValue);
    }

    public class Option<T> : Option, IValueDescriptor<T>, IValueDescriptor, ICompletionSource 
    {
        public Option(string name, string description = null);
        public Option(string[] aliases, string description = null);
        public Option(string name, ParseArgument<T> parseArgument, bool isDefault = False, string description = null);
        public Option(string[] aliases, ParseArgument<T> parseArgument, bool isDefault = False, string description = null);
        public Option(string name, Func<T> getDefaultValue, string description = null);
        public Option(string[] aliases, Func<T> getDefaultValue, string description = null);
        public ArgumentArity Arity { get; set; }
    }

    public static class OptionExtensions 
    {
        public static TOption AddCompletions<TOption>(this TOption option, string[] values);
        public static TOption AddCompletions<TOption>(this TOption option, Func<CompletionContext,IEnumerable<string>> complete);
        public static TOption AddCompletions<TOption>(this TOption option, CompletionDelegate complete);
        public static Option<FileInfo> ExistingOnly(this Option<FileInfo> option);
        public static Option<DirectoryInfo> ExistingOnly(this Option<DirectoryInfo> option);
        public static Option<FileSystemInfo> ExistingOnly(this Option<FileSystemInfo> option);
        public static Option<T> ExistingOnly<T>(this Option<T> option);
        public static TOption FromAmong<TOption>(this TOption option, string[] values);
        public static TOption LegalFileNamesOnly<TOption>(this TOption option);
        public static TOption LegalFilePathsOnly<TOption>(this TOption option);
        public static ParseResult Parse(this Option option, string commandLine);
        public static ParseResult Parse(this Option option, string[] args);
    }

    public class RootCommand : Command, IEnumerable<Symbol>, IEnumerable, ICompletionSource 
    {
        public RootCommand(string description = null);
        public static string ExecutableName { get; }
        public static string ExecutablePath { get; }
    }

    public abstract class Symbol : ICompletionSource 
    {
        public string Description { get; set; }
        public bool IsHidden { get; set; }
        public string Name { get; set; }
        public IEnumerable<Symbol> Parents { get; }
        public IEnumerable<CompletionItem> GetCompletions();
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
    }
}

namespace System.CommandLine.Binding
{    
    public abstract class BinderBase<T> : IValueDescriptor<T>, IValueDescriptor, IValueSource 
    {
        protected T GetBoundValue(BindingContext bindingContext);
    }

    public class BindingContext : IServiceProvider 
    {
        public IConsole Console { get; }
        public ParseResult ParseResult { get; }
        public void AddService(Type serviceType, Func<IServiceProvider,object> factory);
        public void AddService<T>(Func<IServiceProvider,T> factory);
        public object GetService(Type serviceType);
    }

    public struct BoundValue : System.ValueType 
    {
        public static BoundValue DefaultForValueDescriptor(IValueDescriptor valueDescriptor);
        public object Value { get; }
        public IValueDescriptor ValueDescriptor { get; }
        public IValueSource ValueSource { get; }
    }
    
    public interface IValueDescriptor 
    {
        bool HasDefaultValue { get; }
        string ValueName { get; }
        Type ValueType { get; }
        object GetDefaultValue();
    }

    public interface IValueDescriptor<out T> : IValueDescriptor 
    {
    }

    public interface IValueSource 
    {
        bool TryGetValue(IValueDescriptor valueDescriptor, BindingContext bindingContext, ref object& boundValue);
    }
}

namespace System.CommandLine.Builder
{
    public class CommandLineBuilder 
    {
        public CommandLineBuilder(Command rootCommand = null);
        public Command Command { get; }
        public Parser Build()
    }

    public static class CommandLineBuilderExtensions 
    {
        public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, InvocationMiddleware middleware, MiddlewareOrder order = Default);
        public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, Action<InvocationContext> onInvoke, MiddlewareOrder order = Default);
        public static CommandLineBuilder CancelOnProcessTermination(this CommandLineBuilder builder);
        public static CommandLineBuilder EnableDirectives(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder EnableLegacyDoubleDashBehavior(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder EnablePosixBundling(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder RegisterWithDotnetSuggest(this CommandLineBuilder builder);
        public static CommandLineBuilder UseDefaults(this CommandLineBuilder builder);
        public static CommandLineBuilder UseEnvironmentVariableDirective(this CommandLineBuilder builder);
        public static CommandLineBuilder UseExceptionHandler(this CommandLineBuilder builder, Action<Exception,InvocationContext> onException = null, int? errorExitCode = null);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, int? maxWidth = null);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, string[] helpAliases);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, Action<HelpContext> customize, int? maxWidth = null);
        public static TBuilder UseHelpBuilder<TBuilder>(this TBuilder builder, Func<BindingContext,HelpBuilder> getHelpBuilder);
        public static CommandLineBuilder UseLocalizationResources(this CommandLineBuilder builder, LocalizationResources validationMessages);
        public static CommandLineBuilder UseParseDirective(this CommandLineBuilder builder, int? errorExitCode = null);
        public static CommandLineBuilder UseParseErrorReporting(this CommandLineBuilder builder, int? errorExitCode = null);
        public static CommandLineBuilder UseSuggestDirective(this CommandLineBuilder builder);
        public static CommandLineBuilder UseTokenReplacer(this CommandLineBuilder builder, System.CommandLine.Parsing.TryReplaceToken replaceToken);
        public static CommandLineBuilder UseTypoCorrections(this CommandLineBuilder builder, int maxLevenshteinDistance = 3);
        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder);
        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, string[] aliases);
    }
}

namespace System.CommandLine.Completions
{    
    public abstract class CompletionContext 
    {
        public ParseResult ParseResult { get; }
        public string WordToComplete { get; }
    }

    public delegate void CompletionDelegate(CompletionContext context);

    public class CompletionItem 
    {
        public CompletionItem(string label, string kind = Value, string sortText = null, string insertText = null, string documentation = null, string detail = null);
        public string Detail { get; }
        public string Documentation { get; set; }
        public string InsertText { get; }
        public string Kind { get; }
        public string Label { get; }
        public string SortText { get; }
        protected bool Equals(CompletionItem other)
        public bool Equals(object obj);
        public int GetHashCode();
    }
    
    public interface ICompletionSource 
    {
        IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
    }

    public class TextCompletionContext : CompletionContext 
    {
        public string CommandLineText { get; }
        public int CursorPosition { get; }
        public TextCompletionContext AtCursorPosition(int position);
    }

    public class TokenCompletionContext : CompletionContext 
    {
    }
}

namespace System.CommandLine.Help
{
    public class HelpBuilder 
    {
        public HelpBuilder(LocalizationResources localizationResources, int maxWidth = 2147483647);
        public LocalizationResources LocalizationResources { get; }
        public int MaxWidth { get; }
        public void CustomizeLayout(Func<HelpContext,IEnumerable<HelpSectionDelegate>> getLayout);
        public void CustomizeSymbol(Symbol symbol, Func<HelpContext,string> firstColumnText = null, Func<HelpContext,string> secondColumnText = null, Func<HelpContext,string> defaultValue = null);
        public TwoColumnHelpRow GetTwoColumnRow(Symbol symbol, HelpContext context);
        public void Write(HelpContext context);
        public void WriteColumns(IReadOnlyList<TwoColumnHelpRow> items, HelpContext context);
        public static class Default
        {
            public static HelpSectionDelegate AdditionalArgumentsSection();
            public static HelpSectionDelegate CommandArgumentsSection();
            public static HelpSectionDelegate CommandUsageSection();
            public static string GetArgumentDefaultValue(Argument argument);
            public static string GetArgumentDescription(Argument argument);
            public static string GetArgumentUsageLabel(Argument argument);
            public static string GetIdentifierSymbolDescription(IdentifierSymbol symbol);
            public static string GetIdentifierSymbolUsageLabel(IdentifierSymbol symbol, HelpContext context);
            public static IEnumerable<HelpSectionDelegate> GetLayout();
            public static HelpSectionDelegate OptionsSection();
            public static HelpSectionDelegate SubcommandsSection();
            public static HelpSectionDelegate SynopsisSection();
        }
    }

    public static class HelpBuilderExtensions 
    {
        public static void CustomizeSymbol(this HelpBuilder builder, Symbol symbol, string firstColumnText = null, string secondColumnText = null, string defaultValue = null);
        public static void Write(this HelpBuilder helpBuilder, Command command, TextWriter writer);
    }

    public class HelpContext 
    {
        public HelpContext(HelpBuilder helpBuilder, Command command, TextWriter output, ParseResult parseResult = null);
        public Command Command { get; }
        public HelpBuilder HelpBuilder { get; }
        public TextWriter Output { get; }
        public ParseResult ParseResult { get; }
    }

    public delegate void HelpSectionDelegate(HelpContext context);

    public class TwoColumnHelpRow : IEquatable<TwoColumnHelpRow> 
    {
        public TwoColumnHelpRow(string firstColumnText, string secondColumnText);
        public string FirstColumnText { get; }
        public string SecondColumnText { get; }
        public bool Equals(object obj);
        public bool Equals(TwoColumnHelpRow other);
        public int GetHashCode();
    }
}

namespace System.CommandLine.Invocation
{    
    public interface ICommandHandler 
    {
        int Invoke(InvocationContext context);
        Task<int> InvokeAsync(InvocationContext context);
    }

    public interface IInvocationResult 
    {
        void Apply(InvocationContext context);
    }

    public class InvocationContext 
    {
        public InvocationContext(ParseResult parseResult, IConsole console = null);
        public BindingContext BindingContext { get; }
        public IConsole Console { get; set; }
        public int ExitCode { get; set; }
        public HelpBuilder HelpBuilder { get; }
        public IInvocationResult InvocationResult { get; set; }
        public LocalizationResources LocalizationResources { get; }
        public Parser Parser { get; }
        public ParseResult ParseResult { get; set; }
        public System.Threading.CancellationToken GetCancellationToken();
    }

    public delegate void InvocationMiddleware(InvocationContext context, Func<InvocationContext,Task> next);
    
    public enum MiddlewareOrder
    {
        Default = 0,
        ErrorReporting = 1000,
        ExceptionHandler = -2000,
        Configuration = -1000,
    }
}

namespace System.CommandLine.IO
{    
    public interface IStandardError 
    {
        IStandardStreamWriter Error { get; }
        bool IsErrorRedirected { get; }
    }

    public interface IStandardIn 
    {
        bool IsInputRedirected { get; }
    }

    public interface IStandardOut 
    {
        bool IsOutputRedirected { get; }
        IStandardStreamWriter Out { get; }
    }

    public interface IStandardStreamWriter 
    {
        void Write(string value);
    }

    public static class StandardStreamWriter 
    {
        public static IStandardStreamWriter Create(TextWriter writer);
        public static TextWriter CreateTextWriter(this IStandardStreamWriter writer);
        public static void WriteLine(this IStandardStreamWriter writer);
        public static void WriteLine(this IStandardStreamWriter writer, string value);
    }

    public class SystemConsole : IConsole, IStandardError, IStandardIn, IStandardOut 
    {
        public SystemConsole();
        public IStandardStreamWriter Error { get; }
        public bool IsErrorRedirected { get; }
        public bool IsInputRedirected { get; }
        public bool IsOutputRedirected { get; }
        public IStandardStreamWriter Out { get; }
    }

    public class TestConsole : IConsole, IStandardError, IStandardIn, IStandardOut 
    {
        public TestConsole();
        public IStandardStreamWriter Error { get; }
        public bool IsErrorRedirected { get; }
        public bool IsInputRedirected { get; }
        public bool IsOutputRedirected { get; }
        public IStandardStreamWriter Out { get; }
        protected void set_Error(IStandardStreamWriter value);
        protected void set_IsErrorRedirected(bool value);
        protected void set_IsInputRedirected(bool value);
        protected void set_IsOutputRedirected(bool value);
        protected void set_Out(IStandardStreamWriter value);
    }
}

namespace System.CommandLine.Parsing
{
    public class ArgumentResult : SymbolResult 
    {
        public Argument Argument { get; }
        public object GetValueOrDefault();
        public T GetValueOrDefault<T>();
        public void OnlyTake(int numberOfTokens);
    }

    public class CommandLineStringSplitter 
    {
        public IEnumerable<string> Split(string commandLine);
    }

    public class CommandResult : SymbolResult 
    {
        public Command Command { get; }
        public Token Token { get; }
    }

    public class OptionResult : SymbolResult 
    {
        public bool IsImplicit { get; }
        public Option Option { get; }
        public Token Token { get; }
        public object GetValueOrDefault();
        public T GetValueOrDefault<T>();
    }

    public delegate T ParseArgument<out T>(ArgumentResult result);

    public class ParseError 
    {
        public string Message { get; }
        public SymbolResult SymbolResult { get; }
    }

    public class Parser 
    {
        public Parser(CommandLineConfiguration configuration);
        public Parser(Command command);
        public Parser();
        public CommandLineConfiguration Configuration { get; }
        public ParseResult Parse(IReadOnlyList<string> arguments, string rawInput = null)
    }

    public class ParseResult 
    {
        public CommandResult CommandResult { get; }
        public System.CommandLine.DirectiveCollection Directives { get; }
        public IReadOnlyList<ParseError> Errors { get; }
        public Parser Parser { get; }
        public CommandResult RootCommandResult { get; }
        public IReadOnlyList<Token> Tokens { get; }
        public IReadOnlyList<string> UnmatchedTokens { get; }
        public IReadOnlyList<string> UnparsedTokens { get; }
        public ArgumentResult FindResultFor(Argument argument);
        public CommandResult FindResultFor(Command command);
        public OptionResult FindResultFor(Option option);
        public SymbolResult FindResultFor(Symbol symbol);
        public CompletionContext GetCompletionContext();
        public IEnumerable<CompletionItem> GetCompletions(int? position = null);
        public object GetValueForArgument(Argument argument);
        public T GetValueForArgument<T>(Argument<T> argument);
        public object GetValueForOption(Option option);
        public T GetValueForOption<T>(Option<T> option);
    }

    public static class ParseResultExtensions 
    {
        public static string Diagram(this ParseResult parseResult);
        public static bool HasOption(this ParseResult parseResult, Option option);
        public static int Invoke(this ParseResult parseResult, IConsole console = null);
        public static Task<int> InvokeAsync(this ParseResult parseResult, IConsole console = null);
    }

    public static class ParserExtensions 
    {
        public static int Invoke(this Parser parser, string commandLine, IConsole console = null);
        public static int Invoke(this Parser parser, string[] args, IConsole console = null);
        public static Task<int> InvokeAsync(this Parser parser, string commandLine, IConsole console = null);
        public static Task<int> InvokeAsync(this Parser parser, string[] args, IConsole console = null);
        public static ParseResult Parse(this Parser parser, string commandLine);
    }

    public abstract class SymbolResult 
    {
        public IReadOnlyList<SymbolResult> Children { get; }
        public string ErrorMessage { get; set; }
        public LocalizationResources LocalizationResources { get; set; }
        public SymbolResult Parent { get; }
        public Symbol Symbol { get; }
        public IReadOnlyList<Token> Tokens { get; }
        public ArgumentResult FindResultFor(Argument argument);
        public CommandResult FindResultFor(Command command);
        public OptionResult FindResultFor(Option option);
        public T GetValueForArgument<T>(Argument<T> argument);
        public object GetValueForArgument(Argument argument);
        public T GetValueForOption<T>(Option<T> option);
        public object GetValueForOption(Option option);
    }

    public class Token : IEquatable<Token> 
    {
        public Token(string value, TokenType type, Symbol symbol);
        public static bool op_Equality(Token left, Token right);
        public static bool op_Inequality(Token left, Token right);
        public TokenType Type { get; }
        public string Value { get; }
        public bool Equals(object obj);
        public bool Equals(Token other);
        public int GetHashCode();
    }

    public enum TokenType
    {
        Argument = 0,
        Command = 1,
        Option = 2,
        DoubleDash = 3,
        Unparsed = 4,
        Directive = 5,
    }

    public delegate bool TryReplaceToken(string tokenToReplace, ref IReadOnlyList<string> replacementTokens, ref string& errorMessage);

    public delegate void ValidateSymbolResult<in T>(T symbolResult);
}

Samples

Simple sample

This is a CLI with a single root command and a single option which has an argument:

using System.CommandLine;

internal class Program
{
    private static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "The file to read and display on the console.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddOption(fileOption);

        rootCommand.SetHandler(
            (FileInfo file) => ReadFile(file),
            fileOption);
        return await rootCommand.InvokeAsync(args);
    }

    private static void ReadFile(FileInfo file)
        => File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
}

Complex sample

This sample has multiple commands with numerous options. The first option does custom validation:

using System.CommandLine;

namespace scl;

class Program
{
    static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "An option whose argument is parsed as a FileInfo",
            isDefault: true,
            parseArgument: result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("sampleQuotes.txt");

                }
                var filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.ErrorMessage = "File does not exist";
                    return null;
                }
                else
                {
                    return new FileInfo(filePath);
                }
            });

        var delayOption = new Option<int>(
            name: "--delay",
            description: "Delay between lines, specified as milliseconds per character in a line.",
            getDefaultValue: () => 42);

        var fgcolorOption = new Option<ConsoleColor>(
            name: "--fgcolor",
            description: "Foreground color of text displayed on the console.",
            getDefaultValue: () => ConsoleColor.White);

        var lightModeOption = new Option<bool>(
            name: "--light-mode",
            description: "Background color of text displayed on the console: default is black, light mode is white.");

        var searchTermsOption = new Option<string[]>(
            name: "--search-terms",
            description: "Strings to search for when deleting entries.")
            { IsRequired = true, AllowMultipleArgumentsPerToken = true };

        var quoteArgument = new Argument<string>(
            name: "quote",
            description: "Text of quote.");

        var bylineArgument = new Argument<string>(
            name: "byline",
            description: "Byline of quote.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddGlobalOption(fileOption);

        var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
        rootCommand.AddCommand(quotesCommand);

        var readCommand = new Command("read", "Read and display the file.")
            {
                delayOption,
                fgcolorOption,
                lightModeOption
            };
        quotesCommand.AddCommand(readCommand);

        var deleteCommand = new Command("delete", "Delete lines from the file.");
        deleteCommand.AddOption(searchTermsOption);
        quotesCommand.AddCommand(deleteCommand);

        var addCommand = new Command("add", "Add an entry to the file.");
        addCommand.AddArgument(quoteArgument);
        addCommand.AddArgument(bylineArgument);
        addCommand.AddAlias("insert");
        quotesCommand.AddCommand(addCommand);

        readCommand.SetHandler(async
            (FileInfo file, int delay, ConsoleColor fgcolor, bool lightMode) =>
        {
            await ReadFile(file, delay, fgcolor, lightMode);
        },
                fileOption, delayOption, fgcolorOption, lightModeOption);

        deleteCommand.SetHandler(
            (FileInfo file, string[] searchTerms) =>
            {
                DeleteFromFile(file, searchTerms);
            },
            fileOption, searchTermsOption);

        addCommand.SetHandler(
            (FileInfo file, string quote, string byline) =>
            {
                AddToFile(file, quote, byline);
            },
            fileOption, quoteArgument, bylineArgument);

        return await rootCommand.InvokeAsync(args);
    }

    internal static async Task ReadFile(
                FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        var lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };

    }
    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
        File.WriteAllLines(
            file.FullName, File.ReadLines(file.FullName)
                .Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
    }
    internal static void AddToFile(FileInfo file, string quote, string byline)
    {
        Console.WriteLine("Adding to file");
        using var writer = file.AppendText();
        writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
        writer.WriteLine($"{Environment.NewLine}-{byline}");
        writer.Flush();
    }
}
@dotnet-issue-labeler
Copy link

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.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Apr 26, 2022
@KathleenDollard KathleenDollard added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Apr 26, 2022
@jkotas
Copy link
Member

jkotas commented Apr 26, 2022

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.

@jkotas
Copy link
Member

jkotas commented Apr 27, 2022

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.

@jonsequitur
Copy link
Contributor

Would you recommend reducing the number of overloads then? This could make it easier to find the synchronous versus asynchronous variations within the list.

@jkotas
Copy link
Member

jkotas commented Apr 27, 2022

Would you recommend reducing the number of overloads then?

Yes. I think having the generic overloads for up to 8 options would be enough.

@tannergooding
Copy link
Member

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.

@tannergooding
Copy link
Member

Normalizing to keywords (System.String to string, etc) would also help with the review

@ghost
Copy link

ghost commented Apr 27, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

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. 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, 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. 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, 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

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:

System.CommandLine
  public abstract class Argument : Symbol, System.CommandLine.Binding.IValueDescriptor, System.CommandLine.Completions.ICompletionSource
    public ArgumentArity Arity { get; set; }
    public CompletionSourceList Completions { get; }
    public System.Boolean HasDefaultValue { get; }
    public System.String HelpName { get; set; }
    public System.Type ValueType { get; }
    public System.Void AddValidator(System.CommandLine.Parsing.ValidateSymbolResult<System.CommandLine.Parsing.ArgumentResult> validate)
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
    public System.Object GetDefaultValue()
    public System.Void SetDefaultValue(System.Object value)
    public System.Void SetDefaultValueFactory(System.Func<System.Object> getDefaultValue)
    public System.Void SetDefaultValueFactory(System.Func<System.CommandLine.Parsing.ArgumentResult,System.Object> getDefaultValue)
    public System.String ToString()
  public class Argument<T> : Argument, IValueDescriptor<T>, System.CommandLine.Binding.IValueDescriptor, System.CommandLine.Completions.ICompletionSource
    .ctor()
    .ctor(System.String name, System.String description = null)
    .ctor(System.String name, Func<T> getDefaultValue, System.String description = null)
    .ctor(Func<T> getDefaultValue)
    .ctor(System.String name, ParseArgument<T> parse, System.Boolean isDefault = False, System.String description = null)
    .ctor(ParseArgument<T> parse, System.Boolean isDefault = False)
    public System.Type ValueType { get; }
  public struct ArgumentArity : System.ValueType, System.IEquatable<ArgumentArity>
    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; }
    .ctor(System.Int32 minimumNumberOfValues, System.Int32 maximumNumberOfValues)
    public System.Int32 MaximumNumberOfValues { get; }
    public System.Int32 MinimumNumberOfValues { get; }
    public System.Boolean Equals(ArgumentArity other)
    public System.Boolean Equals(System.Object obj)
    public System.Int32 GetHashCode()
  public static class ArgumentExtensions
    public static TArgument AddCompletions<TArgument>(this TArgument argument, System.String[] values)
    public static TArgument AddCompletions<TArgument>(this TArgument argument, System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> complete)
    public static TArgument AddCompletions<TArgument>(this TArgument argument, System.CommandLine.Completions.CompletionDelegate complete)
    public static Argument<System.IO.FileInfo> ExistingOnly(this Argument<System.IO.FileInfo> argument)
    public static Argument<System.IO.DirectoryInfo> ExistingOnly(this Argument<System.IO.DirectoryInfo> argument)
    public static Argument<System.IO.FileSystemInfo> ExistingOnly(this Argument<System.IO.FileSystemInfo> argument)
    public static Argument<T> ExistingOnly<T>(this Argument<T> argument)
    public static TArgument FromAmong<TArgument>(this TArgument argument, System.String[] values)
    public static TArgument LegalFileNamesOnly<TArgument>(this TArgument argument)
    public static TArgument LegalFilePathsOnly<TArgument>(this TArgument argument)
    public static System.CommandLine.Parsing.ParseResult Parse(this Argument argument, System.String commandLine)
    public static System.CommandLine.Parsing.ParseResult Parse(this Argument argument, System.String[] args)
  public class Command : IdentifierSymbol, System.Collections.Generic.IEnumerable<Symbol>, System.Collections.IEnumerable, System.CommandLine.Completions.ICompletionSource
    .ctor(System.String name, System.String description = null)
    public System.Collections.Generic.IReadOnlyList<Argument> Arguments { get; }
    public System.Collections.Generic.IEnumerable<Symbol> Children { get; }
    public System.CommandLine.Invocation.ICommandHandler Handler { get; set; }
    public System.Collections.Generic.IReadOnlyList<Option> Options { get; }
    public System.Collections.Generic.IReadOnlyList<Command> Subcommands { get; }
    public System.Boolean TreatUnmatchedTokensAsErrors { get; set; }
    public System.Void Add(Option option)
    public System.Void Add(Argument argument)
    public System.Void Add(Command command)
    public System.Void AddArgument(Argument argument)
    public System.Void AddCommand(Command command)
    public System.Void AddGlobalOption(Option option)
    public System.Void AddOption(Option option)
    public System.Void AddValidator(System.CommandLine.Parsing.ValidateSymbolResult<System.CommandLine.Parsing.CommandResult> validate)
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
    public System.Collections.Generic.IEnumerator<Symbol> GetEnumerator()
  public static class CommandExtensions
    public static System.Int32 Invoke(this Command command, System.String[] args, IConsole console = null)
    public static System.Int32 Invoke(this Command command, System.String commandLine, IConsole console = null)
    public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String[] args, IConsole console = null)
    public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String commandLine, IConsole console = null)
    public static System.CommandLine.Parsing.ParseResult Parse(this Command command, System.String[] args)
    public static System.CommandLine.Parsing.ParseResult Parse(this Command command, System.String commandLine)
  public class CommandLineConfiguration
    .ctor(Command command, System.Boolean enablePosixBundling = True, System.Boolean enableDirectives = True, System.Boolean enableLegacyDoubleDashBehavior = False, System.Boolean enableTokenReplacement = True, LocalizationResources resources = null, System.Collections.Generic.IReadOnlyList<System.CommandLine.Invocation.InvocationMiddleware> middlewarePipeline = null, System.Func<System.CommandLine.Binding.BindingContext,System.CommandLine.Help.HelpBuilder> helpBuilderFactory = null, System.CommandLine.Parsing.TryReplaceToken tokenReplacer = null)
    public System.Boolean EnableDirectives { get; }
    public System.Boolean EnableLegacyDoubleDashBehavior { get; }
    public System.Boolean EnablePosixBundling { get; }
    public System.Boolean EnableTokenReplacement { get; }
    public LocalizationResources LocalizationResources { get; }
    public Command RootCommand { get; }
    public System.Void ThrowIfInvalid()
  public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable
    .ctor(System.String message)
    .ctor()
    .ctor(System.String message, System.Exception innerException)
  public static class CompletionSourceExtensions
    public static System.Void Add(this CompletionSourceList completionSources, System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> complete)
    public static System.Void Add(this CompletionSourceList completionSources, System.CommandLine.Completions.CompletionDelegate complete)
    public static System.Void Add(this CompletionSourceList completionSources, System.String[] completions)
  public class CompletionSourceList, System.Collections.Generic.IEnumerable<System.CommandLine.Completions.ICompletionSource>, System.Collections.Generic.IReadOnlyCollection<System.CommandLine.Completions.ICompletionSource>, System.Collections.Generic.IReadOnlyList<System.CommandLine.Completions.ICompletionSource>, System.Collections.IEnumerable
    .ctor()
    public System.Int32 Count { get; }
    public System.CommandLine.Completions.ICompletionSource Item { get; }
    public System.Void Add(System.CommandLine.Completions.ICompletionSource source)
    public System.Void Clear()
    public System.Collections.Generic.IEnumerator<System.CommandLine.Completions.ICompletionSource> GetEnumerator()
  public static class ConsoleExtensions
    public static System.Void Write(this IConsole console, System.String value)
    public static System.Void WriteLine(this IConsole console, System.String value)
  public class DirectiveCollection, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.String,System.Collections.Generic.IEnumerable<System.String>>>, System.Collections.IEnumerable
    .ctor()
    public System.Boolean Contains(System.String name)
    public System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String,System.Collections.Generic.IEnumerable<System.String>>> GetEnumerator()
    public System.Boolean TryGetValues(System.String name, ref System.Collections.Generic.IReadOnlyList<System.String> values)
  public static class Handler
    public static System.Void SetHandler(this Command command, System.Action handle)
    public static System.Void SetHandler(this Command command, System.Func<System.Threading.Tasks.Task> handle)
    public static System.Void SetHandler<T>(this Command command, Action<T> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2>(this Command command, Action<T1,T2> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3>(this Command command, Action<T1,T2,T3> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4>(this Command command, Action<T1,T2,T3,T4> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5>(this Command command, Action<T1,T2,T3,T4,T5> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Action<T1,T2,T3,T4,T5,T6> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T>(this Command command, Func<T,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2>(this Command command, Func<T1,T2,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3>(this Command command, Func<T1,T2,T3,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4>(this Command command, Func<T1,T2,T3,T4,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5>(this Command command, Func<T1,T2,T3,T4,T5,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Func<T1,T2,T3,T4,T5,T6,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
    public static System.Void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,System.Threading.Tasks.Task> handle, System.CommandLine.Binding.IValueDescriptor[] symbols)
  public interface IConsole : System.CommandLine.IO.IStandardError, System.CommandLine.IO.IStandardIn, System.CommandLine.IO.IStandardOut
  public abstract class IdentifierSymbol : Symbol, System.CommandLine.Completions.ICompletionSource
    public System.Collections.Generic.IReadOnlyCollection<System.String> Aliases { get; }
    public System.String Name { get; set; }
    public System.Void AddAlias(System.String alias)
    public System.Boolean HasAlias(System.String alias)
  public class LocalizationResources
    public static LocalizationResources Instance { get; }
    public System.String ArgumentConversionCannotParse(System.String value, System.Type expectedType)
    public System.String ArgumentConversionCannotParseForCommand(System.String value, System.String commandAlias, System.Type expectedType)
    public System.String ArgumentConversionCannotParseForOption(System.String value, System.String optionAlias, System.Type expectedType)
    public System.String DirectoryDoesNotExist(System.String path)
    public System.String ErrorReadingResponseFile(System.String filePath, System.IO.IOException e)
    public System.String ExceptionHandlerHeader()
    public System.String ExpectsFewerArguments(System.CommandLine.Parsing.Token token, System.Int32 providedNumberOfValues, System.Int32 maximumNumberOfValues)
    public System.String ExpectsOneArgument(System.CommandLine.Parsing.SymbolResult symbolResult)
    public System.String FileDoesNotExist(System.String filePath)
    public System.String FileOrDirectoryDoesNotExist(System.String path)
    protected System.String GetResourceString(System.String resourceString, System.Object[] formatArguments)
    public System.String HelpAdditionalArgumentsDescription()
    public System.String HelpAdditionalArgumentsTitle()
    public System.String HelpArgumentDefaultValueLabel()
    public System.String HelpArgumentsTitle()
    public System.String HelpCommandsTitle()
    public System.String HelpDescriptionTitle()
    public System.String HelpOptionDescription()
    public System.String HelpOptionsRequiredLabel()
    public System.String HelpOptionsTitle()
    public System.String HelpUsageAdditionalArguments()
    public System.String HelpUsageCommand()
    public System.String HelpUsageOptions()
    public System.String HelpUsageTitle()
    public System.String InvalidCharactersInFileName(System.Char invalidChar)
    public System.String InvalidCharactersInPath(System.Char invalidChar)
    public System.String NoArgumentProvided(System.CommandLine.Parsing.SymbolResult symbolResult)
    public System.String RequiredArgumentMissing(System.CommandLine.Parsing.SymbolResult symbolResult)
    public System.String RequiredCommandWasNotProvided()
    public System.String ResponseFileNotFound(System.String filePath)
    public System.String SuggestionsTokenNotMatched(System.String token)
    public System.String UnrecognizedArgument(System.String unrecognizedArg, System.Collections.Generic.IReadOnlyCollection<System.String> allowedValues)
    public System.String UnrecognizedCommandOrArgument(System.String arg)
    public System.String VersionOptionCannotBeCombinedWithOtherArguments(System.String optionAlias)
    public System.String VersionOptionDescription()
  public abstract class Option : IdentifierSymbol, System.CommandLine.Binding.IValueDescriptor, System.CommandLine.Completions.ICompletionSource
    public System.Boolean AllowMultipleArgumentsPerToken { get; set; }
    public System.String ArgumentHelpName { get; set; }
    public ArgumentArity Arity { get; set; }
    public System.Boolean IsRequired { get; set; }
    public System.Type ValueType { get; }
    public System.Void AddValidator(System.CommandLine.Parsing.ValidateSymbolResult<System.CommandLine.Parsing.OptionResult> validate)
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
    public System.Boolean HasAliasIgnoringPrefix(System.String alias)
    public System.Void SetDefaultValue(System.Object value)
    public System.Void SetDefaultValueFactory(System.Func<System.Object> getDefaultValue)
  public class Option<T> : Option, IValueDescriptor<T>, System.CommandLine.Binding.IValueDescriptor, System.CommandLine.Completions.ICompletionSource
    .ctor(System.String name, System.String description = null)
    .ctor(System.String[] aliases, System.String description = null)
    .ctor(System.String name, ParseArgument<T> parseArgument, System.Boolean isDefault = False, System.String description = null)
    .ctor(System.String[] aliases, ParseArgument<T> parseArgument, System.Boolean isDefault = False, System.String description = null)
    .ctor(System.String name, Func<T> getDefaultValue, System.String description = null)
    .ctor(System.String[] aliases, Func<T> getDefaultValue, System.String description = null)
    public ArgumentArity Arity { get; set; }
  public static class OptionExtensions
    public static TOption AddCompletions<TOption>(this TOption option, System.String[] values)
    public static TOption AddCompletions<TOption>(this TOption option, System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> complete)
    public static TOption AddCompletions<TOption>(this TOption option, System.CommandLine.Completions.CompletionDelegate complete)
    public static Option<System.IO.FileInfo> ExistingOnly(this Option<System.IO.FileInfo> option)
    public static Option<System.IO.DirectoryInfo> ExistingOnly(this Option<System.IO.DirectoryInfo> option)
    public static Option<System.IO.FileSystemInfo> ExistingOnly(this Option<System.IO.FileSystemInfo> option)
    public static Option<T> ExistingOnly<T>(this Option<T> option)
    public static TOption FromAmong<TOption>(this TOption option, System.String[] values)
    public static TOption LegalFileNamesOnly<TOption>(this TOption option)
    public static TOption LegalFilePathsOnly<TOption>(this TOption option)
    public static System.CommandLine.Parsing.ParseResult Parse(this Option option, System.String commandLine)
    public static System.CommandLine.Parsing.ParseResult Parse(this Option option, System.String[] args)
  public class RootCommand : Command, System.Collections.Generic.IEnumerable<Symbol>, System.Collections.IEnumerable, System.CommandLine.Completions.ICompletionSource
    public static System.String ExecutableName { get; }
    public static System.String ExecutablePath { get; }
    .ctor(System.String description = )
  public abstract class Symbol, System.CommandLine.Completions.ICompletionSource
    public System.String Description { get; set; }
    public System.Boolean IsHidden { get; set; }
    public System.String Name { get; set; }
    public System.Collections.Generic.IEnumerable<Symbol> Parents { get; }
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions()
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
    public System.String ToString()
System.CommandLine.Binding
  public abstract class BinderBase<T>, IValueDescriptor<T>, IValueDescriptor, IValueSource
    protected T GetBoundValue(BindingContext bindingContext)
  public class BindingContext, System.IServiceProvider
    public System.CommandLine.IConsole Console { get; }
    public System.CommandLine.Parsing.ParseResult ParseResult { get; }
    public System.Void AddService(System.Type serviceType, System.Func<System.IServiceProvider,System.Object> factory)
    public System.Void AddService<T>(Func<System.IServiceProvider,T> factory)
    public System.Object GetService(System.Type serviceType)
  public struct BoundValue : System.ValueType
    public static BoundValue DefaultForValueDescriptor(IValueDescriptor valueDescriptor)
    public System.Object Value { get; }
    public IValueDescriptor ValueDescriptor { get; }
    public IValueSource ValueSource { get; }
    public System.String ToString()
  public interface IValueDescriptor
    public System.Boolean HasDefaultValue { get; }
    public System.String ValueName { get; }
    public System.Type ValueType { get; }
    public System.Object GetDefaultValue()
  public interface IValueDescriptor<out T> : IValueDescriptor
  public interface IValueSource
    public System.Boolean TryGetValue(IValueDescriptor valueDescriptor, BindingContext bindingContext, ref System.Object& boundValue)
System.CommandLine.Builder
  public class CommandLineBuilder
    .ctor(System.CommandLine.Command rootCommand = null)
    public System.CommandLine.Command Command { get; }
    public System.CommandLine.Parsing.Parser Build()
  public static class CommandLineBuilderExtensions
    public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, System.CommandLine.Invocation.InvocationMiddleware middleware, System.CommandLine.Invocation.MiddlewareOrder order = Default)
    public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, System.Action<System.CommandLine.Invocation.InvocationContext> onInvoke, System.CommandLine.Invocation.MiddlewareOrder order = Default)
    public static CommandLineBuilder CancelOnProcessTermination(this CommandLineBuilder builder)
    public static CommandLineBuilder EnableDirectives(this CommandLineBuilder builder, System.Boolean value = True)
    public static CommandLineBuilder EnableLegacyDoubleDashBehavior(this CommandLineBuilder builder, System.Boolean value = True)
    public static CommandLineBuilder EnablePosixBundling(this CommandLineBuilder builder, System.Boolean value = True)
    public static CommandLineBuilder RegisterWithDotnetSuggest(this CommandLineBuilder builder)
    public static CommandLineBuilder UseDefaults(this CommandLineBuilder builder)
    public static CommandLineBuilder UseEnvironmentVariableDirective(this CommandLineBuilder builder)
    public static CommandLineBuilder UseExceptionHandler(this CommandLineBuilder builder, System.Action<System.Exception,System.CommandLine.Invocation.InvocationContext> onException = null, System.Nullable<System.Int32> errorExitCode = null)
    public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, System.Nullable<System.Int32> maxWidth = null)
    public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, System.String[] helpAliases)
    public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, System.Action<System.CommandLine.Help.HelpContext> customize, System.Nullable<System.Int32> maxWidth = null)
    public static TBuilder UseHelpBuilder<TBuilder>(this TBuilder builder, System.Func<System.CommandLine.Binding.BindingContext,System.CommandLine.Help.HelpBuilder> getHelpBuilder)
    public static CommandLineBuilder UseLocalizationResources(this CommandLineBuilder builder, System.CommandLine.LocalizationResources validationMessages)
    public static CommandLineBuilder UseParseDirective(this CommandLineBuilder builder, System.Nullable<System.Int32> errorExitCode = null)
    public static CommandLineBuilder UseParseErrorReporting(this CommandLineBuilder builder, System.Nullable<System.Int32> errorExitCode = null)
    public static CommandLineBuilder UseSuggestDirective(this CommandLineBuilder builder)
    public static CommandLineBuilder UseTokenReplacer(this CommandLineBuilder builder, System.CommandLine.Parsing.TryReplaceToken replaceToken)
    public static CommandLineBuilder UseTypoCorrections(this CommandLineBuilder builder, System.Int32 maxLevenshteinDistance = 3)
    public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder)
    public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, System.String[] aliases)
System.CommandLine.Completions
  public abstract class CompletionContext
    public System.CommandLine.Parsing.ParseResult ParseResult { get; }
    public System.String WordToComplete { get; }
  public delegate CompletionDelegate : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(CompletionContext context, System.AsyncCallback callback, System.Object object)
    public System.Collections.Generic.IEnumerable<CompletionItem> EndInvoke(System.IAsyncResult result)
    public System.Collections.Generic.IEnumerable<CompletionItem> Invoke(CompletionContext context)
  public class CompletionItem
    .ctor(System.String label, System.String kind = Value, System.String sortText = null, System.String insertText = null, System.String documentation = null, System.String detail = null)
    public System.String Detail { get; }
    public System.String Documentation { get; set; }
    public System.String InsertText { get; }
    public System.String Kind { get; }
    public System.String Label { get; }
    public System.String SortText { get; }
    protected System.Boolean Equals(CompletionItem other)
    public System.Boolean Equals(System.Object obj)
    public System.Int32 GetHashCode()
    public System.String ToString()
  public interface ICompletionSource
    public System.Collections.Generic.IEnumerable<CompletionItem> GetCompletions(CompletionContext context)
  public class TextCompletionContext : CompletionContext
    public System.String CommandLineText { get; }
    public System.Int32 CursorPosition { get; }
    public TextCompletionContext AtCursorPosition(System.Int32 position)
  public class TokenCompletionContext : CompletionContext
System.CommandLine.Help
  public class HelpBuilder
    .ctor(System.CommandLine.LocalizationResources localizationResources, System.Int32 maxWidth = 2147483647)
    public System.CommandLine.LocalizationResources LocalizationResources { get; }
    public System.Int32 MaxWidth { get; }
    public System.Void CustomizeLayout(System.Func<HelpContext,System.Collections.Generic.IEnumerable<HelpSectionDelegate>> getLayout)
    public System.Void CustomizeSymbol(System.CommandLine.Symbol symbol, System.Func<HelpContext,System.String> firstColumnText = null, System.Func<HelpContext,System.String> secondColumnText = null, System.Func<HelpContext,System.String> defaultValue = null)
    public TwoColumnHelpRow GetTwoColumnRow(System.CommandLine.Symbol symbol, HelpContext context)
    public System.Void Write(HelpContext context)
    public System.Void WriteColumns(System.Collections.Generic.IReadOnlyList<TwoColumnHelpRow> items, HelpContext context)
   static class Default
    public static HelpSectionDelegate AdditionalArgumentsSection()
    public static HelpSectionDelegate CommandArgumentsSection()
    public static HelpSectionDelegate CommandUsageSection()
    public static System.String GetArgumentDefaultValue(System.CommandLine.Argument argument)
    public static System.String GetArgumentDescription(System.CommandLine.Argument argument)
    public static System.String GetArgumentUsageLabel(System.CommandLine.Argument argument)
    public static System.String GetIdentifierSymbolDescription(System.CommandLine.IdentifierSymbol symbol)
    public static System.String GetIdentifierSymbolUsageLabel(System.CommandLine.IdentifierSymbol symbol, HelpContext context)
    public static System.Collections.Generic.IEnumerable<HelpSectionDelegate> GetLayout()
    public static HelpSectionDelegate OptionsSection()
    public static HelpSectionDelegate SubcommandsSection()
    public static HelpSectionDelegate SynopsisSection()
  public static class HelpBuilderExtensions
    public static System.Void CustomizeSymbol(this HelpBuilder builder, System.CommandLine.Symbol symbol, System.String firstColumnText = null, System.String secondColumnText = null, System.String defaultValue = null)
    public static System.Void Write(this HelpBuilder helpBuilder, System.CommandLine.Command command, System.IO.TextWriter writer)
  public class HelpContext
    .ctor(HelpBuilder helpBuilder, System.CommandLine.Command command, System.IO.TextWriter output, System.CommandLine.Parsing.ParseResult parseResult = null)
    public System.CommandLine.Command Command { get; }
    public HelpBuilder HelpBuilder { get; }
    public System.IO.TextWriter Output { get; }
    public System.CommandLine.Parsing.ParseResult ParseResult { get; }
  public delegate HelpSectionDelegate : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(HelpContext context, System.AsyncCallback callback, System.Object object)
    public System.Void EndInvoke(System.IAsyncResult result)
    public System.Void Invoke(HelpContext context)
  public class TwoColumnHelpRow, System.IEquatable<TwoColumnHelpRow>
    .ctor(System.String firstColumnText, System.String secondColumnText)
    public System.String FirstColumnText { get; }
    public System.String SecondColumnText { get; }
    public System.Boolean Equals(System.Object obj)
    public System.Boolean Equals(TwoColumnHelpRow other)
    public System.Int32 GetHashCode()
System.CommandLine.Invocation
  public interface ICommandHandler
    public System.Int32 Invoke(InvocationContext context)
    public System.Threading.Tasks.Task<System.Int32> InvokeAsync(InvocationContext context)
  public interface IInvocationResult
    public System.Void Apply(InvocationContext context)
  public class InvocationContext
    .ctor(System.CommandLine.Parsing.ParseResult parseResult, System.CommandLine.IConsole console = null)
    public System.CommandLine.Binding.BindingContext BindingContext { get; }
    public System.CommandLine.IConsole Console { get; set; }
    public System.Int32 ExitCode { get; set; }
    public System.CommandLine.Help.HelpBuilder HelpBuilder { get; }
    public IInvocationResult InvocationResult { get; set; }
    public System.CommandLine.LocalizationResources LocalizationResources { get; }
    public System.CommandLine.Parsing.Parser Parser { get; }
    public System.CommandLine.Parsing.ParseResult ParseResult { get; set; }
    public System.Threading.CancellationToken GetCancellationToken()
  public delegate InvocationMiddleware : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(InvocationContext context, System.Func<InvocationContext,System.Threading.Tasks.Task> next, System.AsyncCallback callback, System.Object object)
    public System.Threading.Tasks.Task EndInvoke(System.IAsyncResult result)
    public System.Threading.Tasks.Task Invoke(InvocationContext context, System.Func<InvocationContext,System.Threading.Tasks.Task> next)
  public enum MiddlewareOrder : System.Enum, System.IComparable, System.IConvertible, System.IFormattable
    Default=0
    ErrorReporting=1000
    ExceptionHandler=-2000
    Configuration=-1000
System.CommandLine.IO
  public interface IStandardError
    public IStandardStreamWriter Error { get; }
    public System.Boolean IsErrorRedirected { get; }
  public interface IStandardIn
    public System.Boolean IsInputRedirected { get; }
  public interface IStandardOut
    public System.Boolean IsOutputRedirected { get; }
    public IStandardStreamWriter Out { get; }
  public interface IStandardStreamWriter
    public System.Void Write(System.String value)
  public static class StandardStreamWriter
    public static IStandardStreamWriter Create(System.IO.TextWriter writer)
    public static System.IO.TextWriter CreateTextWriter(this IStandardStreamWriter writer)
    public static System.Void WriteLine(this IStandardStreamWriter writer)
    public static System.Void WriteLine(this IStandardStreamWriter writer, System.String value)
  public class SystemConsole, System.CommandLine.IConsole, IStandardError, IStandardIn, IStandardOut
    .ctor()
    public IStandardStreamWriter Error { get; }
    public System.Boolean IsErrorRedirected { get; }
    public System.Boolean IsInputRedirected { get; }
    public System.Boolean IsOutputRedirected { get; }
    public IStandardStreamWriter Out { get; }
  public class TestConsole, System.CommandLine.IConsole, IStandardError, IStandardIn, IStandardOut
    .ctor()
    public IStandardStreamWriter Error { get; }
    public System.Boolean IsErrorRedirected { get; }
    public System.Boolean IsInputRedirected { get; }
    public System.Boolean IsOutputRedirected { get; }
    public IStandardStreamWriter Out { get; }
    protected System.Void set_Error(IStandardStreamWriter value)
    protected System.Void set_IsErrorRedirected(System.Boolean value)
    protected System.Void set_IsInputRedirected(System.Boolean value)
    protected System.Void set_IsOutputRedirected(System.Boolean value)
    protected System.Void set_Out(IStandardStreamWriter value)
System.CommandLine.Parsing
  public class ArgumentResult : SymbolResult
    public System.CommandLine.Argument Argument { get; }
    public System.Object GetValueOrDefault()
    public T GetValueOrDefault<T>()
    public System.Void OnlyTake(System.Int32 numberOfTokens)
    public System.String ToString()
  public class CommandLineStringSplitter
    public System.Collections.Generic.IEnumerable<System.String> Split(System.String commandLine)
  public class CommandResult : SymbolResult
    public System.CommandLine.Command Command { get; }
    public Token Token { get; }
  public class OptionResult : SymbolResult
    public System.Boolean IsImplicit { get; }
    public System.CommandLine.Option Option { get; }
    public Token Token { get; }
    public System.Object GetValueOrDefault()
    public T GetValueOrDefault<T>()
  public delegate ParseArgument<out T> : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(ArgumentResult result, System.AsyncCallback callback, System.Object object)
    public T EndInvoke(System.IAsyncResult result)
    public T Invoke(ArgumentResult result)
  public class ParseError
    public System.String Message { get; }
    public SymbolResult SymbolResult { get; }
    public System.String ToString()
  public class Parser
    .ctor(System.CommandLine.CommandLineConfiguration configuration)
    .ctor(System.CommandLine.Command command)
    .ctor()
    public System.CommandLine.CommandLineConfiguration Configuration { get; }
    public ParseResult Parse(System.Collections.Generic.IReadOnlyList<System.String> arguments, System.String rawInput = null)
  public class ParseResult
    public CommandResult CommandResult { get; }
    public System.CommandLine.DirectiveCollection Directives { get; }
    public System.Collections.Generic.IReadOnlyList<ParseError> Errors { get; }
    public Parser Parser { get; }
    public CommandResult RootCommandResult { get; }
    public System.Collections.Generic.IReadOnlyList<Token> Tokens { get; }
    public System.Collections.Generic.IReadOnlyList<System.String> UnmatchedTokens { get; }
    public System.Collections.Generic.IReadOnlyList<System.String> UnparsedTokens { get; }
    public ArgumentResult FindResultFor(System.CommandLine.Argument argument)
    public CommandResult FindResultFor(System.CommandLine.Command command)
    public OptionResult FindResultFor(System.CommandLine.Option option)
    public SymbolResult FindResultFor(System.CommandLine.Symbol symbol)
    public System.CommandLine.Completions.CompletionContext GetCompletionContext()
    public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.Nullable<System.Int32> position = null)
    public System.Object GetValueForArgument(System.CommandLine.Argument argument)
    public T GetValueForArgument<T>(Argument<T> argument)
    public System.Object GetValueForOption(System.CommandLine.Option option)
    public T GetValueForOption<T>(Option<T> option)
    public System.String ToString()
  public static class ParseResultExtensions
    public static System.String Diagram(this ParseResult parseResult)
    public static System.Boolean HasOption(this ParseResult parseResult, System.CommandLine.Option option)
    public static System.Int32 Invoke(this ParseResult parseResult, System.CommandLine.IConsole console = null)
    public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this ParseResult parseResult, System.CommandLine.IConsole console = null)
  public static class ParserExtensions
    public static System.Int32 Invoke(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
    public static System.Int32 Invoke(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
    public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
    public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
    public static ParseResult Parse(this Parser parser, System.String commandLine)
  public abstract class SymbolResult
    public System.Collections.Generic.IReadOnlyList<SymbolResult> Children { get; }
    public System.String ErrorMessage { get; set; }
    public System.CommandLine.LocalizationResources LocalizationResources { get; set; }
    public SymbolResult Parent { get; }
    public System.CommandLine.Symbol Symbol { get; }
    public System.Collections.Generic.IReadOnlyList<Token> Tokens { get; }
    public ArgumentResult FindResultFor(System.CommandLine.Argument argument)
    public CommandResult FindResultFor(System.CommandLine.Command command)
    public OptionResult FindResultFor(System.CommandLine.Option option)
    public T GetValueForArgument<T>(Argument<T> argument)
    public System.Object GetValueForArgument(System.CommandLine.Argument argument)
    public T GetValueForOption<T>(Option<T> option)
    public System.Object GetValueForOption(System.CommandLine.Option option)
    public System.String ToString()
  public class Token, System.IEquatable<Token>
    public static System.Boolean op_Equality(Token left, Token right)
    public static System.Boolean op_Inequality(Token left, Token right)
    .ctor(System.String value, TokenType type, System.CommandLine.Symbol symbol)
    public TokenType Type { get; }
    public System.String Value { get; }
    public System.Boolean Equals(System.Object obj)
    public System.Boolean Equals(Token other)
    public System.Int32 GetHashCode()
    public System.String ToString()
  public enum TokenType : System.Enum, System.IComparable, System.IConvertible, System.IFormattable
    Argument=0
    Command=1
    Option=2
    DoubleDash=3
    Unparsed=4
    Directive=5
  public delegate TryReplaceToken : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(System.String tokenToReplace, ref System.Collections.Generic.IReadOnlyList<System.String> replacementTokens, ref System.String& errorMessage, System.AsyncCallback callback, System.Object object)
    public System.Boolean EndInvoke(ref System.Collections.Generic.IReadOnlyList<System.String> replacementTokens, ref System.String& errorMessage, System.IAsyncResult result)
    public System.Boolean Invoke(System.String tokenToReplace, ref System.Collections.Generic.IReadOnlyList<System.String> replacementTokens, ref System.String& errorMessage)
  public delegate ValidateSymbolResult<in T> : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
    .ctor(System.Object object, System.IntPtr method)
    public System.IAsyncResult BeginInvoke(T symbolResult, System.AsyncCallback callback, System.Object object)
    public System.Void EndInvoke(System.IAsyncResult result)
    public System.Void Invoke(T symbolResult)

Samples

Simple sample

This is a CLI with a single root command and a single option which has an argument:

using System.CommandLine;

internal class Program
{
    private static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "The file to read and display on the console.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddOption(fileOption);

        rootCommand.SetHandler(
            (FileInfo file) => ReadFile(file),
            fileOption);
        return await rootCommand.InvokeAsync(args);
    }

    private static void ReadFile(FileInfo file)
        => File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
}

Complex sample

This sample has multiple commands with numerous options. The first option does custom validation:

using System.CommandLine;

namespace scl;

class Program
{
    static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "An option whose argument is parsed as a FileInfo",
            isDefault: true,
            parseArgument: result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("sampleQuotes.txt");

                }
                var filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.ErrorMessage = "File does not exist";
                    return null;
                }
                else
                {
                    return new FileInfo(filePath);
                }
            });

        var delayOption = new Option<int>(
            name: "--delay",
            description: "Delay between lines, specified as milliseconds per character in a line.",
            getDefaultValue: () => 42);

        var fgcolorOption = new Option<ConsoleColor>(
            name: "--fgcolor",
            description: "Foreground color of text displayed on the console.",
            getDefaultValue: () => ConsoleColor.White);

        var lightModeOption = new Option<bool>(
            name: "--light-mode",
            description: "Background color of text displayed on the console: default is black, light mode is white.");

        var searchTermsOption = new Option<string[]>(
            name: "--search-terms",
            description: "Strings to search for when deleting entries.")
            { IsRequired = true, AllowMultipleArgumentsPerToken = true };

        var quoteArgument = new Argument<string>(
            name: "quote",
            description: "Text of quote.");

        var bylineArgument = new Argument<string>(
            name: "byline",
            description: "Byline of quote.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddGlobalOption(fileOption);

        var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
        rootCommand.AddCommand(quotesCommand);

        var readCommand = new Command("read", "Read and display the file.")
            {
                delayOption,
                fgcolorOption,
                lightModeOption
            };
        quotesCommand.AddCommand(readCommand);

        var deleteCommand = new Command("delete", "Delete lines from the file.");
        deleteCommand.AddOption(searchTermsOption);
        quotesCommand.AddCommand(deleteCommand);

        var addCommand = new Command("add", "Add an entry to the file.");
        addCommand.AddArgument(quoteArgument);
        addCommand.AddArgument(bylineArgument);
        addCommand.AddAlias("insert");
        quotesCommand.AddCommand(addCommand);

        readCommand.SetHandler(async
            (FileInfo file, int delay, ConsoleColor fgcolor, bool lightMode) =>
        {
            await ReadFile(file, delay, fgcolor, lightMode);
        },
                fileOption, delayOption, fgcolorOption, lightModeOption);

        deleteCommand.SetHandler(
            (FileInfo file, string[] searchTerms) =>
            {
                DeleteFromFile(file, searchTerms);
            },
            fileOption, searchTermsOption);

        addCommand.SetHandler(
            (FileInfo file, string quote, string byline) =>
            {
                AddToFile(file, quote, byline);
            },
            fileOption, quoteArgument, bylineArgument);

        return await rootCommand.InvokeAsync(args);
    }

    internal static async Task ReadFile(
                FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        var lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };

    }
    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
        File.WriteAllLines(
            file.FullName, File.ReadLines(file.FullName)
                .Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
    }
    internal static void AddToFile(FileInfo file, string quote, string byline)
    {
        Console.WriteLine("Adding to file");
        using var writer = file.AppendText();
        writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
        writer.WriteLine($"{Environment.NewLine}-{byline}");
        writer.Flush();
    }
}
Author: KathleenDollard
Assignees: -
Labels:

area-System.Runtime, blocking, untriaged, api-ready-for-review

Milestone: -

@tannergooding
Copy link
Member

tannergooding commented Apr 27, 2022

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?

@tannergooding
Copy link
Member

@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.

@terrajobst
Copy link
Member

I know. I asked them to file this issue. It's already scheduled for tomorrow.

@terrajobst terrajobst added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Apr 28, 2022
@davidfowl
Copy link
Member

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.

@jkotas
Copy link
Member

jkotas commented May 5, 2022

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.

@davidfowl
Copy link
Member

Just checking 😄

@ghost
Copy link

ghost commented May 9, 2022

Tagging subscribers to this area: @dotnet/area-system-console
See info in area-owners.md if you want to be subscribed.

Issue Details

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. 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, 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. 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, 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

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:

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<ArgumentResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public object GetDefaultValue();
        public void SetDefaultValue(object value);
        public void SetDefaultValueFactory(Func<object> getDefaultValue);
        public void SetDefaultValueFactory(Func<ArgumentResult,object> getDefaultValue);
    }

    public class Argument<T> : Argument, IValueDescriptor<T>, IValueDescriptor, ICompletionSource 
    {
        public Argument();
        public Argument(string name, string description = null);
        public Argument(string name, Func<T> getDefaultValue, string description = null);
        public Argument(Func<T> getDefaultValue);
        public Argument(string name, ParseArgument<T> parse, bool isDefault = False, string description = null);
        public Argument(ParseArgument<T> parse, bool isDefault = False);
        public Type ValueType { get; }
    }

    public struct ArgumentArity : System.ValueType : IEquatable<ArgumentArity> 
    {
        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<TArgument>(this TArgument argument, string[] values);
        public static TArgument AddCompletions<TArgument>(this TArgument argument, Func<CompletionContext,IEnumerable<string>> complete);
        public static TArgument AddCompletions<TArgument>(this TArgument argument, CompletionDelegate complete);
        public static Argument<FileInfo> ExistingOnly(this Argument<FileInfo> argument);
        public static Argument<DirectoryInfo> ExistingOnly(this Argument<DirectoryInfo> argument);
        public static Argument<FileSystemInfo> ExistingOnly(this Argument<FileSystemInfo> argument);
        public static Argument<T> ExistingOnly<T>(this Argument<T> argument);
        public static TArgument FromAmong<TArgument>(this TArgument argument, string[] values);
        public static TArgument LegalFileNamesOnly<TArgument>(this TArgument argument);
        public static TArgument LegalFilePathsOnly<TArgument>(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<Symbol>, IEnumerable, ICompletionSource 
    {
        public Command(string name, string description = null);
        public IReadOnlyList<Argument> Arguments { get; }
        public IEnumerable<Symbol> Children { get; }
        public ICommandHandler Handler { get; set; }
        public IReadOnlyList<Option> Options { get; }
        public IReadOnlyList<Command> Subcommands { get; }
        public bool TreatUnmatchedTokensAsErrors { get; set; }
        public void Add(Option option);
        public void Add(Argument argument);
        public void Add(Command command);
        public void AddArgument(Argument argument);
        public void AddCommand(Command command);
        public void AddGlobalOption(Option option);
        public void AddOption(Option option);
        public void AddValidator(ValidateSymbolResult<CommandResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public IEnumerator<Symbol> GetEnumerator();
    }

    public static class CommandExtensions 
    {
        public static int Invoke(this Command command, string[] args, IConsole console = null);
        public static int Invoke(this Command command, string commandLine, IConsole console = null);
        public static Task<int> InvokeAsync(this Command command, string[] args, IConsole console = null);
        public static Task<int> InvokeAsync(this Command command, string commandLine, IConsole console = null);
        public static ParseResult Parse(this Command command, string[] args);
        public static ParseResult Parse(this Command command, string commandLine);
    }

    public class CommandLineConfiguration 
    {
        public CommandLineConfiguration(Command command, bool enablePosixBundling = True, bool enableDirectives = True, bool enableLegacyDoubleDashBehavior = False, bool enableTokenReplacement = True, LocalizationResources resources = null, IReadOnlyList<InvocationMiddleware> middlewarePipeline = null, Func<BindingContext,HelpBuilder> helpBuilderFactory = null, System.CommandLine.Parsing.TryReplaceToken tokenReplacer = null);
        public bool EnableDirectives { get; }
        public bool EnableLegacyDoubleDashBehavior { get; }
        public bool EnablePosixBundling { get; }
        public bool EnableTokenReplacement { get; }
        public LocalizationResources LocalizationResources { get; }
        public Command RootCommand { get; }
        public void ThrowIfInvalid();
    }

    public class CommandLineConfigurationException : Exception, System.Runtime.Serialization.ISerializable 
    {
        public CommandLineConfigurationException(string message);
        public CommandLineConfigurationException();
        public CommandLineConfigurationException(string message, Exception innerException);
    }

    public static class CompletionSourceExtensions 
    {
        public static void Add(this CompletionSourceList completionSources, Func<CompletionContext,IEnumerable<string>> complete);
        public static void Add(this CompletionSourceList completionSources, CompletionDelegate complete);
        public static void Add(this CompletionSourceList completionSources, string[] completions);
    }

    public class CompletionSourceList : IEnumerable<ICompletionSource>, IReadOnlyCollection<ICompletionSource>, IReadOnlyList<ICompletionSource>, IEnumerable 
    {
        public CompletionSourceList();
        public int Count { get; }
        public ICompletionSource Item { get; }
        public void Add(ICompletionSource source);
        public void Clear();
        public IEnumerator<ICompletionSource> GetEnumerator();
    }

    public static class ConsoleExtensions 
    {
        public static void Write(this IConsole console, string value);
        public static void WriteLine(this IConsole console, string value);
    }

    public class DirectiveCollection : IEnumerable<KeyValuePair<string,IEnumerable<string>>>, IEnumerable 
    {
        public DirectiveCollection();
        public bool Contains(string name);
        public IEnumerator<KeyValuePair<string,IEnumerable<string>>> GetEnumerator();
        public bool TryGetValues(string name, ref IReadOnlyList<string> values);
    }

    public static class Handler 
    {
        public static void SetHandler(this Command command, Action handle);
        public static void SetHandler(this Command command, Func<Task> handle);
        public static void SetHandler<T>(this Command command, Action<T> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2>(this Command command, Action<T1,T2> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3>(this Command command, Action<T1,T2,T3> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4>(this Command command, Action<T1,T2,T3,T4> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5>(this Command command, Action<T1,T2,T3,T4,T5> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Action<T1,T2,T3,T4,T5,T6> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T>(this Command command, Func<T,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2>(this Command command, Func<T1,T2,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3>(this Command command, Func<T1,T2,T3,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4>(this Command command, Func<T1,T2,T3,T4,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5>(this Command command, Func<T1,T2,T3,T4,T5,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6>(this Command command, Func<T1,T2,T3,T4,T5,T6,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,Task> handle, IValueDescriptor[] symbols);
        public static void SetHandler<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Command command, Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,Task> handle, IValueDescriptor[] symbols);
    }

    public interface IConsole : IStandardError, IStandardIn, IStandardOut 
    {
    }

    public abstract class IdentifierSymbol : Symbol, ICompletionSource 
    {
        public IReadOnlyCollection<string> Aliases { get; }
        public string Name { get; set; }
        public void AddAlias(string alias);
        public bool HasAlias(string alias);
    }

    public class LocalizationResources 
    {
        public static LocalizationResources Instance { get; }
        public string ArgumentConversionCannotParse(string value, Type expectedType);
        public string ArgumentConversionCannotParseForCommand(string value, string commandAlias, Type expectedType);
        public string ArgumentConversionCannotParseForOption(string value, string optionAlias, Type expectedType);
        public string DirectoryDoesNotExist(string path);
        public string ErrorReadingResponseFile(string filePath, System.IO.IOException e);
        public string ExceptionHandlerHeader();
        public string ExpectsFewerArguments(Token token, int providedNumberOfValues, int maximumNumberOfValues);
        public string ExpectsOneArgument(SymbolResult symbolResult);
        public string FileDoesNotExist(string filePath);
        public string FileOrDirectoryDoesNotExist(string path);
        protected string GetResourceString(string resourceString, object[] formatArguments);
        public string HelpAdditionalArgumentsDescription();
        public string HelpAdditionalArgumentsTitle();
        public string HelpArgumentDefaultValueLabel();
        public string HelpArgumentsTitle();
        public string HelpCommandsTitle();
        public string HelpDescriptionTitle();
        public string HelpOptionDescription();
        public string HelpOptionsRequiredLabel();
        public string HelpOptionsTitle();
        public string HelpUsageAdditionalArguments();
        public string HelpUsageCommand();
        public string HelpUsageOptions();
        public string HelpUsageTitle();
        public string InvalidCharactersInFileName(System.Char invalidChar);
        public string InvalidCharactersInPath(System.Char invalidChar);
        public string NoArgumentProvided(SymbolResult symbolResult);
        public string RequiredArgumentMissing(SymbolResult symbolResult);
        public string RequiredCommandWasNotProvided();
        public string ResponseFileNotFound(string filePath);
        public string SuggestionsTokenNotMatched(string token);
        public string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCollection<string> allowedValues);
        public string UnrecognizedCommandOrArgument(string arg);
        public string VersionOptionCannotBeCombinedWithOtherArguments(string optionAlias);
        public string VersionOptionDescription();
    }

    public abstract class Option : IdentifierSymbol, IValueDescriptor, ICompletionSource 
    {
        public bool AllowMultipleArgumentsPerToken { get; set; }
        public string ArgumentHelpName { get; set; }
        public ArgumentArity Arity { get; set; }
        public bool IsRequired { get; set; }
        public Type ValueType { get; }
        public void AddValidator(ValidateSymbolResult<OptionResult> validate);
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
        public bool HasAliasIgnoringPrefix(string alias);
        public void SetDefaultValue(object value);
        public void SetDefaultValueFactory(Func<object> getDefaultValue);
    }

    public class Option<T> : Option, IValueDescriptor<T>, IValueDescriptor, ICompletionSource 
    {
        public Option(string name, string description = null);
        public Option(string[] aliases, string description = null);
        public Option(string name, ParseArgument<T> parseArgument, bool isDefault = False, string description = null);
        public Option(string[] aliases, ParseArgument<T> parseArgument, bool isDefault = False, string description = null);
        public Option(string name, Func<T> getDefaultValue, string description = null);
        public Option(string[] aliases, Func<T> getDefaultValue, string description = null);
        public ArgumentArity Arity { get; set; }
    }

    public static class OptionExtensions 
    {
        public static TOption AddCompletions<TOption>(this TOption option, string[] values);
        public static TOption AddCompletions<TOption>(this TOption option, Func<CompletionContext,IEnumerable<string>> complete);
        public static TOption AddCompletions<TOption>(this TOption option, CompletionDelegate complete);
        public static Option<FileInfo> ExistingOnly(this Option<FileInfo> option);
        public static Option<DirectoryInfo> ExistingOnly(this Option<DirectoryInfo> option);
        public static Option<FileSystemInfo> ExistingOnly(this Option<FileSystemInfo> option);
        public static Option<T> ExistingOnly<T>(this Option<T> option);
        public static TOption FromAmong<TOption>(this TOption option, string[] values);
        public static TOption LegalFileNamesOnly<TOption>(this TOption option);
        public static TOption LegalFilePathsOnly<TOption>(this TOption option);
        public static ParseResult Parse(this Option option, string commandLine);
        public static ParseResult Parse(this Option option, string[] args);
    }

    public class RootCommand : Command, IEnumerable<Symbol>, IEnumerable, ICompletionSource 
    {
        public RootCommand(string description = null);
        public static string ExecutableName { get; }
        public static string ExecutablePath { get; }
    }

    public abstract class Symbol : ICompletionSource 
    {
        public string Description { get; set; }
        public bool IsHidden { get; set; }
        public string Name { get; set; }
        public IEnumerable<Symbol> Parents { get; }
        public IEnumerable<CompletionItem> GetCompletions();
        public IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
    }
}

namespace System.CommandLine.Binding
{    
    public abstract class BinderBase<T> : IValueDescriptor<T>, IValueDescriptor, IValueSource 
    {
        protected T GetBoundValue(BindingContext bindingContext);
    }

    public class BindingContext : IServiceProvider 
    {
        public IConsole Console { get; }
        public ParseResult ParseResult { get; }
        public void AddService(Type serviceType, Func<IServiceProvider,object> factory);
        public void AddService<T>(Func<IServiceProvider,T> factory);
        public object GetService(Type serviceType);
    }

    public struct BoundValue : System.ValueType 
    {
        public static BoundValue DefaultForValueDescriptor(IValueDescriptor valueDescriptor);
        public object Value { get; }
        public IValueDescriptor ValueDescriptor { get; }
        public IValueSource ValueSource { get; }
    }
    
    public interface IValueDescriptor 
    {
        bool HasDefaultValue { get; }
        string ValueName { get; }
        Type ValueType { get; }
        object GetDefaultValue();
    }

    public interface IValueDescriptor<out T> : IValueDescriptor 
    {
    }

    public interface IValueSource 
    {
        bool TryGetValue(IValueDescriptor valueDescriptor, BindingContext bindingContext, ref object& boundValue);
    }
}

namespace System.CommandLine.Builder
{
    public class CommandLineBuilder 
    {
        public CommandLineBuilder(Command rootCommand = null);
        public Command Command { get; }
        public Parser Build()
    }

    public static class CommandLineBuilderExtensions 
    {
        public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, InvocationMiddleware middleware, MiddlewareOrder order = Default);
        public static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder, Action<InvocationContext> onInvoke, MiddlewareOrder order = Default);
        public static CommandLineBuilder CancelOnProcessTermination(this CommandLineBuilder builder);
        public static CommandLineBuilder EnableDirectives(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder EnableLegacyDoubleDashBehavior(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder EnablePosixBundling(this CommandLineBuilder builder, bool value = True);
        public static CommandLineBuilder RegisterWithDotnetSuggest(this CommandLineBuilder builder);
        public static CommandLineBuilder UseDefaults(this CommandLineBuilder builder);
        public static CommandLineBuilder UseEnvironmentVariableDirective(this CommandLineBuilder builder);
        public static CommandLineBuilder UseExceptionHandler(this CommandLineBuilder builder, Action<Exception,InvocationContext> onException = null, int? errorExitCode = null);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, int? maxWidth = null);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, string[] helpAliases);
        public static CommandLineBuilder UseHelp(this CommandLineBuilder builder, Action<HelpContext> customize, int? maxWidth = null);
        public static TBuilder UseHelpBuilder<TBuilder>(this TBuilder builder, Func<BindingContext,HelpBuilder> getHelpBuilder);
        public static CommandLineBuilder UseLocalizationResources(this CommandLineBuilder builder, LocalizationResources validationMessages);
        public static CommandLineBuilder UseParseDirective(this CommandLineBuilder builder, int? errorExitCode = null);
        public static CommandLineBuilder UseParseErrorReporting(this CommandLineBuilder builder, int? errorExitCode = null);
        public static CommandLineBuilder UseSuggestDirective(this CommandLineBuilder builder);
        public static CommandLineBuilder UseTokenReplacer(this CommandLineBuilder builder, System.CommandLine.Parsing.TryReplaceToken replaceToken);
        public static CommandLineBuilder UseTypoCorrections(this CommandLineBuilder builder, int maxLevenshteinDistance = 3);
        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder);
        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, string[] aliases);
    }
}

namespace System.CommandLine.Completions
{    
    public abstract class CompletionContext 
    {
        public ParseResult ParseResult { get; }
        public string WordToComplete { get; }
    }

    public delegate void CompletionDelegate(CompletionContext context);

    public class CompletionItem 
    {
        public CompletionItem(string label, string kind = Value, string sortText = null, string insertText = null, string documentation = null, string detail = null);
        public string Detail { get; }
        public string Documentation { get; set; }
        public string InsertText { get; }
        public string Kind { get; }
        public string Label { get; }
        public string SortText { get; }
        protected bool Equals(CompletionItem other)
        public bool Equals(object obj);
        public int GetHashCode();
    }
    
    public interface ICompletionSource 
    {
        IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
    }

    public class TextCompletionContext : CompletionContext 
    {
        public string CommandLineText { get; }
        public int CursorPosition { get; }
        public TextCompletionContext AtCursorPosition(int position);
    }

    public class TokenCompletionContext : CompletionContext 
    {
    }
}

namespace System.CommandLine.Help
{
    public class HelpBuilder 
    {
        public HelpBuilder(LocalizationResources localizationResources, int maxWidth = 2147483647);
        public LocalizationResources LocalizationResources { get; }
        public int MaxWidth { get; }
        public void CustomizeLayout(Func<HelpContext,IEnumerable<HelpSectionDelegate>> getLayout);
        public void CustomizeSymbol(Symbol symbol, Func<HelpContext,string> firstColumnText = null, Func<HelpContext,string> secondColumnText = null, Func<HelpContext,string> defaultValue = null);
        public TwoColumnHelpRow GetTwoColumnRow(Symbol symbol, HelpContext context);
        public void Write(HelpContext context);
        public void WriteColumns(IReadOnlyList<TwoColumnHelpRow> items, HelpContext context);
        public static class Default
        {
            public static HelpSectionDelegate AdditionalArgumentsSection();
            public static HelpSectionDelegate CommandArgumentsSection();
            public static HelpSectionDelegate CommandUsageSection();
            public static string GetArgumentDefaultValue(Argument argument);
            public static string GetArgumentDescription(Argument argument);
            public static string GetArgumentUsageLabel(Argument argument);
            public static string GetIdentifierSymbolDescription(IdentifierSymbol symbol);
            public static string GetIdentifierSymbolUsageLabel(IdentifierSymbol symbol, HelpContext context);
            public static IEnumerable<HelpSectionDelegate> GetLayout();
            public static HelpSectionDelegate OptionsSection();
            public static HelpSectionDelegate SubcommandsSection();
            public static HelpSectionDelegate SynopsisSection();
        }
    }

    public static class HelpBuilderExtensions 
    {
        public static void CustomizeSymbol(this HelpBuilder builder, Symbol symbol, string firstColumnText = null, string secondColumnText = null, string defaultValue = null);
        public static void Write(this HelpBuilder helpBuilder, Command command, TextWriter writer);
    }

    public class HelpContext 
    {
        public HelpContext(HelpBuilder helpBuilder, Command command, TextWriter output, ParseResult parseResult = null);
        public Command Command { get; }
        public HelpBuilder HelpBuilder { get; }
        public TextWriter Output { get; }
        public ParseResult ParseResult { get; }
    }

    public delegate void HelpSectionDelegate(HelpContext context);

    public class TwoColumnHelpRow : IEquatable<TwoColumnHelpRow> 
    {
        public TwoColumnHelpRow(string firstColumnText, string secondColumnText);
        public string FirstColumnText { get; }
        public string SecondColumnText { get; }
        public bool Equals(object obj);
        public bool Equals(TwoColumnHelpRow other);
        public int GetHashCode();
    }
}

namespace System.CommandLine.Invocation
{    
    public interface ICommandHandler 
    {
        int Invoke(InvocationContext context);
        Task<int> InvokeAsync(InvocationContext context);
    }

    public interface IInvocationResult 
    {
        void Apply(InvocationContext context);
    }

    public class InvocationContext 
    {
        public InvocationContext(ParseResult parseResult, IConsole console = null);
        public BindingContext BindingContext { get; }
        public IConsole Console { get; set; }
        public int ExitCode { get; set; }
        public HelpBuilder HelpBuilder { get; }
        public IInvocationResult InvocationResult { get; set; }
        public LocalizationResources LocalizationResources { get; }
        public Parser Parser { get; }
        public ParseResult ParseResult { get; set; }
        public System.Threading.CancellationToken GetCancellationToken();
    }

    public delegate void InvocationMiddleware(InvocationContext context, Func<InvocationContext,Task> next);
    
    public enum MiddlewareOrder
    {
        Default = 0,
        ErrorReporting = 1000,
        ExceptionHandler = -2000,
        Configuration = -1000,
    }
}

namespace System.CommandLine.IO
{    
    public interface IStandardError 
    {
        IStandardStreamWriter Error { get; }
        bool IsErrorRedirected { get; }
    }

    public interface IStandardIn 
    {
        bool IsInputRedirected { get; }
    }

    public interface IStandardOut 
    {
        bool IsOutputRedirected { get; }
        IStandardStreamWriter Out { get; }
    }

    public interface IStandardStreamWriter 
    {
        void Write(string value);
    }

    public static class StandardStreamWriter 
    {
        public static IStandardStreamWriter Create(TextWriter writer);
        public static TextWriter CreateTextWriter(this IStandardStreamWriter writer);
        public static void WriteLine(this IStandardStreamWriter writer);
        public static void WriteLine(this IStandardStreamWriter writer, string value);
    }

    public class SystemConsole : IConsole, IStandardError, IStandardIn, IStandardOut 
    {
        public SystemConsole();
        public IStandardStreamWriter Error { get; }
        public bool IsErrorRedirected { get; }
        public bool IsInputRedirected { get; }
        public bool IsOutputRedirected { get; }
        public IStandardStreamWriter Out { get; }
    }

    public class TestConsole : IConsole, IStandardError, IStandardIn, IStandardOut 
    {
        public TestConsole();
        public IStandardStreamWriter Error { get; }
        public bool IsErrorRedirected { get; }
        public bool IsInputRedirected { get; }
        public bool IsOutputRedirected { get; }
        public IStandardStreamWriter Out { get; }
        protected void set_Error(IStandardStreamWriter value);
        protected void set_IsErrorRedirected(bool value);
        protected void set_IsInputRedirected(bool value);
        protected void set_IsOutputRedirected(bool value);
        protected void set_Out(IStandardStreamWriter value);
    }
}

namespace System.CommandLine.Parsing
{
    public class ArgumentResult : SymbolResult 
    {
        public Argument Argument { get; }
        public object GetValueOrDefault();
        public T GetValueOrDefault<T>();
        public void OnlyTake(int numberOfTokens);
    }

    public class CommandLineStringSplitter 
    {
        public IEnumerable<string> Split(string commandLine);
    }

    public class CommandResult : SymbolResult 
    {
        public Command Command { get; }
        public Token Token { get; }
    }

    public class OptionResult : SymbolResult 
    {
        public bool IsImplicit { get; }
        public Option Option { get; }
        public Token Token { get; }
        public object GetValueOrDefault();
        public T GetValueOrDefault<T>();
    }

    public delegate T ParseArgument<out T>(ArgumentResult result);

    public class ParseError 
    {
        public string Message { get; }
        public SymbolResult SymbolResult { get; }
    }

    public class Parser 
    {
        public Parser(CommandLineConfiguration configuration);
        public Parser(Command command);
        public Parser();
        public CommandLineConfiguration Configuration { get; }
        public ParseResult Parse(IReadOnlyList<string> arguments, string rawInput = null)
    }

    public class ParseResult 
    {
        public CommandResult CommandResult { get; }
        public System.CommandLine.DirectiveCollection Directives { get; }
        public IReadOnlyList<ParseError> Errors { get; }
        public Parser Parser { get; }
        public CommandResult RootCommandResult { get; }
        public IReadOnlyList<Token> Tokens { get; }
        public IReadOnlyList<string> UnmatchedTokens { get; }
        public IReadOnlyList<string> UnparsedTokens { get; }
        public ArgumentResult FindResultFor(Argument argument);
        public CommandResult FindResultFor(Command command);
        public OptionResult FindResultFor(Option option);
        public SymbolResult FindResultFor(Symbol symbol);
        public CompletionContext GetCompletionContext();
        public IEnumerable<CompletionItem> GetCompletions(int? position = null);
        public object GetValueForArgument(Argument argument);
        public T GetValueForArgument<T>(Argument<T> argument);
        public object GetValueForOption(Option option);
        public T GetValueForOption<T>(Option<T> option);
    }

    public static class ParseResultExtensions 
    {
        public static string Diagram(this ParseResult parseResult);
        public static bool HasOption(this ParseResult parseResult, Option option);
        public static int Invoke(this ParseResult parseResult, IConsole console = null);
        public static Task<int> InvokeAsync(this ParseResult parseResult, IConsole console = null);
    }

    public static class ParserExtensions 
    {
        public static int Invoke(this Parser parser, string commandLine, IConsole console = null);
        public static int Invoke(this Parser parser, string[] args, IConsole console = null);
        public static Task<int> InvokeAsync(this Parser parser, string commandLine, IConsole console = null);
        public static Task<int> InvokeAsync(this Parser parser, string[] args, IConsole console = null);
        public static ParseResult Parse(this Parser parser, string commandLine);
    }

    public abstract class SymbolResult 
    {
        public IReadOnlyList<SymbolResult> Children { get; }
        public string ErrorMessage { get; set; }
        public LocalizationResources LocalizationResources { get; set; }
        public SymbolResult Parent { get; }
        public Symbol Symbol { get; }
        public IReadOnlyList<Token> Tokens { get; }
        public ArgumentResult FindResultFor(Argument argument);
        public CommandResult FindResultFor(Command command);
        public OptionResult FindResultFor(Option option);
        public T GetValueForArgument<T>(Argument<T> argument);
        public object GetValueForArgument(Argument argument);
        public T GetValueForOption<T>(Option<T> option);
        public object GetValueForOption(Option option);
    }

    public class Token : IEquatable<Token> 
    {
        public Token(string value, TokenType type, Symbol symbol);
        public static bool op_Equality(Token left, Token right);
        public static bool op_Inequality(Token left, Token right);
        public TokenType Type { get; }
        public string Value { get; }
        public bool Equals(object obj);
        public bool Equals(Token other);
        public int GetHashCode();
    }

    public enum TokenType
    {
        Argument = 0,
        Command = 1,
        Option = 2,
        DoubleDash = 3,
        Unparsed = 4,
        Directive = 5,
    }

    public delegate bool TryReplaceToken(string tokenToReplace, ref IReadOnlyList<string> replacementTokens, ref string& errorMessage);

    public delegate void ValidateSymbolResult<in T>(T symbolResult);
}

Samples

Simple sample

This is a CLI with a single root command and a single option which has an argument:

using System.CommandLine;

internal class Program
{
    private static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "The file to read and display on the console.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddOption(fileOption);

        rootCommand.SetHandler(
            (FileInfo file) => ReadFile(file),
            fileOption);
        return await rootCommand.InvokeAsync(args);
    }

    private static void ReadFile(FileInfo file)
        => File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
}

Complex sample

This sample has multiple commands with numerous options. The first option does custom validation:

using System.CommandLine;

namespace scl;

class Program
{
    static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "An option whose argument is parsed as a FileInfo",
            isDefault: true,
            parseArgument: result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("sampleQuotes.txt");

                }
                var filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.ErrorMessage = "File does not exist";
                    return null;
                }
                else
                {
                    return new FileInfo(filePath);
                }
            });

        var delayOption = new Option<int>(
            name: "--delay",
            description: "Delay between lines, specified as milliseconds per character in a line.",
            getDefaultValue: () => 42);

        var fgcolorOption = new Option<ConsoleColor>(
            name: "--fgcolor",
            description: "Foreground color of text displayed on the console.",
            getDefaultValue: () => ConsoleColor.White);

        var lightModeOption = new Option<bool>(
            name: "--light-mode",
            description: "Background color of text displayed on the console: default is black, light mode is white.");

        var searchTermsOption = new Option<string[]>(
            name: "--search-terms",
            description: "Strings to search for when deleting entries.")
            { IsRequired = true, AllowMultipleArgumentsPerToken = true };

        var quoteArgument = new Argument<string>(
            name: "quote",
            description: "Text of quote.");

        var bylineArgument = new Argument<string>(
            name: "byline",
            description: "Byline of quote.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddGlobalOption(fileOption);

        var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
        rootCommand.AddCommand(quotesCommand);

        var readCommand = new Command("read", "Read and display the file.")
            {
                delayOption,
                fgcolorOption,
                lightModeOption
            };
        quotesCommand.AddCommand(readCommand);

        var deleteCommand = new Command("delete", "Delete lines from the file.");
        deleteCommand.AddOption(searchTermsOption);
        quotesCommand.AddCommand(deleteCommand);

        var addCommand = new Command("add", "Add an entry to the file.");
        addCommand.AddArgument(quoteArgument);
        addCommand.AddArgument(bylineArgument);
        addCommand.AddAlias("insert");
        quotesCommand.AddCommand(addCommand);

        readCommand.SetHandler(async
            (FileInfo file, int delay, ConsoleColor fgcolor, bool lightMode) =>
        {
            await ReadFile(file, delay, fgcolor, lightMode);
        },
                fileOption, delayOption, fgcolorOption, lightModeOption);

        deleteCommand.SetHandler(
            (FileInfo file, string[] searchTerms) =>
            {
                DeleteFromFile(file, searchTerms);
            },
            fileOption, searchTermsOption);

        addCommand.SetHandler(
            (FileInfo file, string quote, string byline) =>
            {
                AddToFile(file, quote, byline);
            },
            fileOption, quoteArgument, bylineArgument);

        return await rootCommand.InvokeAsync(args);
    }

    internal static async Task ReadFile(
                FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        var lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };

    }
    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
        File.WriteAllLines(
            file.FullName, File.ReadLines(file.FullName)
                .Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
    }
    internal static void AddToFile(FileInfo file, string quote, string byline)
    {
        Console.WriteLine("Adding to file");
        using var writer = file.AppendText();
        writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
        writer.WriteLine($"{Environment.NewLine}-{byline}");
        writer.Flush();
    }
}
Author: KathleenDollard
Assignees: -
Labels:

area-System.Console, blocking, untriaged, api-ready-for-review

Milestone: -

@jeffhandley jeffhandley removed the untriaged New issue has not been triaged by the area owner label May 12, 2022
@GrabYourPitchforks GrabYourPitchforks removed the blocking Marks issues that we want to fast track in order to unblock other important work label May 31, 2022
@GrabYourPitchforks
Copy link
Member

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! :)

@terrajobst terrajobst added this to the 7.0.0 milestone May 31, 2022
@deeprobin
Copy link
Contributor

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);
    }

@deeprobin
Copy link
Contributor

& Why is Token not a (readonly) struct?

@KalleOlaviNiemitalo
Copy link

Why is Token not a (readonly) struct?

To minimise JIT compilation time during startup, according to dotnet/command-line-api#1654.

@KalleOlaviNiemitalo
Copy link

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.

@iSazonov
Copy link
Contributor

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.

@jonsequitur
Copy link
Contributor

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.

@KieranDevvs
Copy link

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?

@jonsequitur
Copy link
Contributor

@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.

@adamsitnik
Copy link
Member

adamsitnik commented Mar 30, 2023

Background and motivation

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:

  • Building a hierarchy of symbols
  • Completions
  • Parsing the hierarchy for given input
  • Execution of the parsed result (an optional step, not included in this proposal)

We expect these APIs to be building blocks for:

  • Advanced users who build fully featured CLIs.
  • Our incoming source-generator based solution (not included in this proposal), which aims for simplicity and not needing the users to become familiar with most of the concepts related to parsing.
// 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;
}

Reviewing by Concept rather than full Type

Symbols

namespace System.CommandLine;

/// <summary>
/// Defines a named symbol that resides in a hierarchy with parent and child symbols.
/// </summary>
public abstract class CliSymbol
{
    /// <summary>
    /// Gets or sets the description of the symbol.
    /// </summary>
    public string? Description { get; set; }

    /// <summary>
    /// Gets the name of the symbol.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the symbol is hidden.
    /// </summary>
    public bool IsHidden { get; set; }

    /// <summary>
    /// Gets the parent symbols.
    /// </summary>
    public IEnumerable<CliSymbol> Parents { get; }
}

/// <summary>
/// A symbol defining a value that can be passed on the command line to a <see cref="CliCommand">command</see> or <see cref="CliOption">option</see>.
/// </summary>
public abstract class CliArgument : CliSymbol
{
    /// <summary>
    /// The name used in help output to describe the argument. 
    /// </summary>
    /// Example: `dotnet build --help`
    /// Arguments:
    /// <PROJECT | SOLUTION>  The project or solution file to operate on.
    /// "PROJECT | SOLUTION" is the HelpName here
    public string? HelpName { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.
    /// </summary>
    public abstract Type ValueType { get; }
}

public class CliArgument<T> : CliArgument
{
    /// <summary>
    /// Initializes a new instance of the Argument class.
    /// </summary>
    /// <param name="name">The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.</param>>
    public CliArgument(string name);

    /// <inheritdoc />
    public override Type ValueType => typeof(T);
}

/// <summary>
/// A symbol defining a named parameter and a value for that parameter. 
/// </summary>
public abstract class CliOption : CliSymbol
{
    /// <summary>
    /// Gets or sets the name of the Option when displayed in help.
    /// </summary>
    /// <value>
    /// The name of the option when displayed in help.
    /// </value>
    /// <remarks>Useful for localization, as it's not used for actual parsing.</remarks>
    public string? HelpName { get; set; }

    /// <summary>
    /// When set to true, this option will be applied to the command and recursively to subcommands.
    /// It will not apply to parent commands.
    /// </summary>
    public bool AppliesToSelfAndChildren { get; set; }

    /// <summary>
    /// Indicates whether the option is required when its parent command is invoked.
    /// </summary>
    /// <remarks>When an option is required and its parent command is invoked without it, an error results.</remarks>
    public bool IsRequired { get; set; }

    /// <summary>
    /// Gets the unique set of strings that can be used on the command line to specify the Option.
    /// </summary>
    /// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Option.</remarks>
    public ICollection<string> Aliases { get; }
}

public class CliOption<T> : CliOption
{
    /// <summary>
    /// Initializes a new instance of the Option class.
    /// </summary>
    /// <param name="name">The name of the option. It's used for parsing, displaying Help and creating parse errors.</param>>
    /// <param name="aliases">Optional aliases. Used for parsing, suggestions and displayed in Help.</param>
    public CliOption(string name, params string[] aliases);
}

/// <summary>
/// Represents a specific action that the application performs.
/// </summary>
/// <remarks>
/// Use the Command object for actions that correspond to a specific string (the command name). See
/// <see cref="RootCommand"/> for simple applications that only have one action. For example, <c>dotnet run</c>
/// uses <c>run</c> as the command.
/// </remarks>
public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
    /// <summary>
    /// Initializes a new instance of the Command class.
    /// </summary>
    /// <param name="name">The name of the command.</param>
    /// <param name="description">The description of the command, shown in help.</param>
    public CliCommand(string name, string? description = null);

    /// <summary>
    /// Gets the child symbols.
    /// </summary>
    public IEnumerable<CliSymbol> Children { get; }

    /// <summary>
    /// Represents all of the arguments for the command.
    /// </summary>
    public IList<CliArgument> Arguments { get; }

    /// <summary>
    /// Represents all of the options for the command, including global options that have been applied to any of the command's ancestors.
    /// </summary>
    public IList<CliOption> Options { get; }

    /// <summary>
    /// Represents all of the subcommands for the command.
    /// </summary>
    public IList<CliCommand> Subcommands { get; }

    /// <summary>
    /// Gets the unique set of strings that can be used on the command line to specify the command.
    /// </summary>
    /// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Command.</remarks>
    public ICollection<string> Aliases { get; }

    /// <summary>
    /// Adds a <see cref="CliSymbol"/> to the command.
    /// </summary>
    /// <param name="symbol">The symbol to add to the command.</param>
    [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# duck typing
    public void Add(CliSymbol symbol);

    /// <summary>
    /// Represents all of the symbols for the command.
    /// </summary>
    public IEnumerator<CliSymbol> GetEnumerator() => Children.GetEnumerator();

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

/// <summary>
/// The purpose of directives is to provide cross-cutting functionality that can apply across command-line apps.
/// Because directives are syntactically distinct from the app's own syntax, they can provide functionality that applies across apps.
/// 
/// A directive must conform to the following syntax rules:
/// * It's a token on the command line that comes after the app's name but before any subcommands or options.
/// * It's enclosed in square brackets.
/// * It doesn't contain spaces.
/// </summary>
public class CliDirective : CliSymbol
{
    /// <summary>
    /// Initializes a new instance of the Directive class.
    /// </summary>
    /// <param name="name">The name of the directive. It can't contain whitespaces.</param>
    public CliDirective(string name);
}

/// <summary>
/// Enables the use of the <c>[env:key=value]</c> directive, allowing environment variables to be set from the command line during invocation.
/// </summary>
public sealed class EnvironmentVariablesDirective : CliDirective
{
    public EnvironmentVariablesDirective();
}

/// <summary>
/// Enables the use of the <c>[suggest]</c> directive which when specified in command line input short circuits normal command handling and writes a newline-delimited list of suggestions suitable for use by most shells to provide command line completions.
/// </summary>
/// <remarks>The <c>dotnet-suggest</c> tool requires the suggest directive to be enabled for an application to provide completions.</remarks>
public sealed class SuggestDirective : CliDirective
{
    public SuggestDirective();
}

/// <summary>
/// Enables the use of the <c>[diagram]</c> directive, which when specified on the command line will short 
/// circuit normal command handling and display a diagram explaining the parse result for the command line input.
/// </summary>
/// Example: dotnet [diagram] build -c Release -f net7.0
/// Output: [ dotnet [ build [ -c <Release> ] [ -f <net7.0> ] ] ]
public sealed class DiagramDirective : CliDirective
{
    /// <param name="errorExitCode">If the parse result contains errors, this exit code will be used when the process exits.</param>
    public DiagramDirective(int errorExitCode = DefaultErrorExitCode);
}

Parsing

namespace System.CommandLine.Parsing

/// <summary>
/// Identifies the type of a <see cref="CliToken"/>.
/// </summary>
public enum TokenType
{
    /// <summary>
    /// An argument token.
    /// </summary>
    /// <see cref="CliArgument"/>
    Argument,

    /// <summary>
    /// A command token.
    /// </summary>
    /// <see cref="CliCommand"/>
    Command,
            
    /// <summary>
    /// An option token.
    /// </summary>
    /// <see cref="CliOption"/>
    Option,
            
    /// <summary>
    /// A double dash (<c>--</c>) token, which changes the meaning of subsequent tokens.
    /// </summary>
    DoubleDash,

    /// <summary>
    /// A directive token.
    /// </summary>
    /// <see cref="System.CommandLine.CliDirective"/>
    Directive
}

/// <summary>
/// A unit of significant text on the command line.
/// </summary>
public sealed class CliToken : IEquatable<CliToken>
{
    /// <param name="value">The string value of the token.</param>
    /// <param name="type">The type of the token.</param>
    /// <param name="symbol">The symbol represented by the token</param>
    public CliToken(string? value, TokenType type, CliSymbol symbol);

    /// <summary>
    /// The string value of the token.
    /// </summary>
    public string Value { get; }

    /// <summary>
    /// The type of the token.
    /// </summary>
    public TokenType Type { get; }
}

/// <summary>
/// Defines the arity of an option or argument.
/// </summary>
/// <remarks>The arity refers to the number of values that can be passed on the command line.
/// </remarks>
public readonly struct ArgumentArity : IEquatable<ArgumentArity>
{
    /// <summary>
    /// Initializes a new instance of the ArgumentArity class.
    /// </summary>
    /// <param name="minimumNumberOfValues">The minimum number of values required for the argument.</param>
    /// <param name="maximumNumberOfValues">The maximum number of values allowed for the argument.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="minimumNumberOfValues"/> is negative.</exception>
    /// <exception cref="ArgumentException">Thrown when the maximum number is less than the minimum number or the maximum number is greater than MaximumArity.</exception>
    public ArgumentArity(int minimumNumberOfValues, int maximumNumberOfValues);

    /// <summary>
    /// Gets the minimum number of values required for an <see cref="CliArgument">argument</see>.
    /// </summary>
    public int MinimumNumberOfValues { get; }

    /// <summary>
    /// Gets the maximum number of values allowed for an <see cref="CliArgument">argument</see>.
    /// </summary>
    public int MaximumNumberOfValues { get; }

    /// <summary>
    /// An arity that does not allow any values.
    /// </summary>
    public static ArgumentArity Zero => new(0, 0);

    /// <summary>
    /// An arity that may have one value, but no more than one.
    /// </summary>
    public static ArgumentArity ZeroOrOne => new(0, 1);

    /// <summary>
    /// An arity that must have exactly one value.
    /// </summary>
    public static ArgumentArity ExactlyOne => new(1, 1);

    /// <summary>
    /// An arity that may have multiple values.
    /// </summary>
    public static ArgumentArity ZeroOrMore => new(0, MaximumArity);

    /// <summary>
    /// An arity that must have at least one value.
    /// </summary>
    public static ArgumentArity OneOrMore => new(1, MaximumArity);
}

/// <summary>
/// Describes an error that occurs while parsing command line input.
/// </summary>
public sealed class ParseError
{
    /// <summary>
    /// A message to explain the error to a user.
    /// </summary>
    public string Message { get; }

    /// <summary>
    /// The symbol result detailing the symbol that failed to parse and the tokens involved.
    /// </summary>
    public SymbolResult? SymbolResult { get; }
}

/// <summary>
/// A result produced during parsing for a specific symbol.
/// </summary>
public abstract class SymbolResult
{
    /// <summary>
    /// The parent symbol result in the parse tree.
    /// </summary>
    public SymbolResult? Parent { get; }

    /// <summary>
    /// The list of tokens associated with this symbol result during parsing.
    /// </summary>
    public IReadOnlyList<CliToken> Tokens { get; }

    /// <summary>
    /// Adds an error message for this symbol result to it's parse tree.
    /// </summary>
    /// <remarks>Setting an error will cause the parser to indicate an error for the user and prevent invocation of the command line.</remarks>
    public virtual void AddError(string errorMessage);

    /// <summary>
    /// Finds a result for the specific argument anywhere in the parse tree, including parent and child symbol results.
    /// </summary>
    /// <param name="argument">The argument for which to find a result.</param>
    /// <returns>An argument result if the argument was matched by the parser or has a default value; otherwise, <c>null</c>.</returns>
    public ArgumentResult? FindResultFor(CliArgument argument);

    public CommandResult? FindResultFor(CliCommand command);

    public OptionResult? FindResultFor(CliOption option);

    public DirectiveResult? FindResultFor(CliDirective directive);

    public T? GetValue<T>(CliArgument<T> argument);

    public T? GetValue<T>(CliOption<T> option);
}

/// <summary>
/// A result produced when parsing an <see cref="Argument"/>.
/// </summary>
public sealed class ArgumentResult : SymbolResult
{
    /// <summary>
    /// The argument to which the result applies.
    /// </summary>
    public CliArgument Argument { get; }

    /// <summary>
    /// Gets the parsed value or the default value for <see cref="Argument"/>.
    /// </summary>
    /// <returns>The parsed value or the default value for <see cref="Argument"/></returns>
    public T GetValueOrDefault<T>();

    /// <summary>
    /// Specifies the maximum number of tokens to consume for the argument. Remaining tokens are passed on and can be consumed by later arguments, or will otherwise be added to <see cref="ParseResult.UnmatchedTokens"/>
    /// </summary>
    /// <param name="numberOfTokens">The number of tokens to take. The rest are passed on.</param>
    /// <exception cref="ArgumentOutOfRangeException">numberOfTokens - Value must be at least 1.</exception>
    /// <exception cref="InvalidOperationException">Thrown if this method is called more than once.</exception>
    /// <exception cref="NotSupportedException">Thrown if this method is called by Option-owned ArgumentResult.</exception>
    public void OnlyTake(int numberOfTokens);
}

/// <summary>
/// A result produced when parsing an <see cref="Option" />.
/// </summary>
public sealed class OptionResult : SymbolResult
{
    /// <summary>
    /// The option to which the result applies.
    /// </summary>
    public CliOption Option { get; }

    /// <summary>
    /// Indicates whether the result was created implicitly and not due to the option being specified on the command line.
    /// </summary>
    /// <remarks>Implicit results commonly result from options having a default value.</remarks>
    public bool IsImplicit { get; }

    /// <summary>
    /// The token that was parsed to specify the option.
    /// </summary>
    public CliToken? IdentifierToken { get; }

    /// <summary>
    /// Gets the parsed value or the default value for <see cref="Option"/>.
    /// </summary>
    /// <returns>The parsed value or the default value for <see cref="Option"/></returns>
    public T? GetValueOrDefault<T>();
}

/// <summary>
/// A result produced when parsing an <see cref="CliDirective"/>.
/// </summary>
public sealed class DirectiveResult : SymbolResult
{
    /// <summary>
    /// Parsed values of [name:value] directive(s).
    /// </summary>
    /// <remarks>Can be empty for [name] directives.</remarks>
    public IReadOnlyList<string> Values { get; }

    /// <summary>
    /// The directive to which the result applies.
    /// </summary>
    public CliDirective Directive { get; }

    /// <summary>
    /// The token that was parsed to specify the directive.
    /// </summary>
    public CliToken IdentifierToken { get; }
}

/// <summary>
/// A result produced when parsing a <see cref="Command" />.
/// </summary>
public sealed class CommandResult : SymbolResult
{
    /// <summary>
    /// The command to which the result applies.
    /// </summary>
    public CliCommand Command { get; }

    /// <summary>
    /// The token that was parsed to specify the command.
    /// </summary>
    public CliToken IdentifierToken { get; }

    /// <summary>
    /// Child symbol results in the parse tree.
    /// </summary>
    public IEnumerable<SymbolResult> Children { get; }
}

public abstract class CliArgument : CliSymbol
{
    /// <summary>
    /// Gets or sets the arity of the argument.
    /// </summary>
    public ArgumentArity Arity { get; set; }
    
    /// <summary>
    /// Specifies if a default value is defined for the argument.
    /// </summary>
    public abstract bool HasDefaultValue { get; }

    /// <summary>
    /// Gets the default value for the argument.
    /// </summary>
    /// <returns>Returns the default value for the argument, if defined. Null otherwise.</returns>
    public object? GetDefaultValue();
}

public class CliArgument<T> : CliArgument
{
    /// <summary>
    /// A custom argument parser.
    /// </summary>
    /// <remarks>
    /// It's invoked when there was parse input provided for given Argument.
    /// The same instance can be set as <see cref="DefaultValueFactory"/>, in such case
    /// the delegate is also invoked when no input was provided.
    /// </remarks>
    public Func<ArgumentResult, T>? CustomParser { get; set; }

    /// <summary>
    /// The delegate to invoke to create the default value.
    /// </summary>
    /// <remarks>
    /// It's invoked when there was no parse input provided for given Argument.
    /// The same instance can be set as <see cref="CustomParser"/>, in such case
    /// the delegate is also invoked when an input was provided.
    /// </remarks>
    public Func<ArgumentResult, T>? DefaultValueFactory { get; set; }
}

/// <summary>
/// A symbol defining a named parameter and a value for that parameter. 
/// </summary>
public abstract class CliOption : CliSymbol
{
    /// <summary>
    /// Gets or sets the arity of the option.
    /// </summary>
    public ArgumentArity Arity { get; set; }

    /// <summary>
    /// Gets a value that indicates whether multiple argument tokens are allowed for each option identifier token.
    /// </summary>
    /// <example>
    /// If set to <see langword="true"/>, the following command line is valid for passing multiple arguments:
    /// <code>
    /// > --opt 1 2 3
    /// </code>
    /// The following is equivalent and is always valid:
    /// <code>
    /// > --opt 1 --opt 2 --opt 3
    /// </code>
    /// </example>
    public bool AllowMultipleArgumentsPerToken { get; set; }
}

public class CliOption<T> : CliOption
{
    public Func<ArgumentResult, T>? DefaultValueFactory { get; set; }

    public Func<ArgumentResult, T>? CustomParser { get; set; }
}

public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
    /// <summary>
    /// Gets or sets a value that indicates whether unmatched tokens should be treated as errors. For example,
    /// if set to <see langword="true"/> and an extra command or argument is provided, validation will fail.
    /// </summary>
    public bool TreatUnmatchedTokensAsErrors { get; set; } = true;

    /// <summary>
    /// Parses an array strings using the command.
    /// </summary>
    /// <param name="args">The string arguments to parse.</param>
    /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
    /// <returns>A parse result describing the outcome of the parse operation.</returns>
    public ParseResult Parse(IReadOnlyList<string> args, CliConfiguration? configuration = null);

    /// <summary>
    /// Parses a command line string value using the command.
    /// </summary>
    /// <remarks>The command line string input will be split into tokens as if it had been passed on the command line.</remarks>
    /// <param name="commandLine">A command line string to parse, which can include spaces and quotes equivalent to what can be entered into a terminal.</param>
    /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
    /// <returns>A parse result describing the outcome of the parse operation.</returns>
    public ParseResult Parse(string commandLine, CliConfiguration? configuration = null);
}

/// <summary>
/// Replaces a token with one or more other tokens prior to parsing.
/// </summary>
public delegate bool TryReplaceToken(
    string tokenToReplace,
    out IReadOnlyList<string>? replacementTokens, 
    out string? errorMessage);

// <summary>
/// Represents the configuration used by the <see cref="CliParser"/>.
/// </summary>
public class CliConfiguration
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CliConfiguration"/> class.
    /// </summary>
    /// <param name="rootCommand">The root command for the parser.</param>
    public CliConfiguration(CliCommand rootCommand);

    /// <summary>
    /// Gets a mutable list of the enabled directives.
    /// Currently only <see cref="SuggestDirective"/> is enabled by default.
    /// </summary>
    public List<CliDirective> Directives { get; }

    /// <summary>
    /// Enables the parser to recognize and expand POSIX-style bundled options.
    /// </summary>
    /// <param name="value"><see langword="true"/> to parse POSIX bundles; otherwise, <see langword="false"/>.</param>
    /// <remarks>
    /// POSIX conventions recommend that single-character options be allowed to be specified together after a single <c>-</c> prefix. When <see cref="EnablePosixBundling"/> is set to <see langword="true"/>, the following command lines are equivalent:
    /// 
    /// <code>
    ///     &gt; myapp -a -b -c
    ///     &gt; myapp -abc
    /// </code>
    /// 
    /// If an argument is provided after an option bundle, it applies to the last option in the bundle. When <see cref="EnablePosixBundling"/> is set to <see langword="true"/>, all of the following command lines are equivalent:
    /// <code>
    ///     &gt; myapp -a -b -c arg
    ///     &gt; myapp -abc arg
    ///     &gt; myapp -abcarg
    /// </code>
    ///
    /// </remarks>
    public bool EnablePosixBundling { get; set; } = true;

    /// <summary>
    /// Enables a default exception handler to catch any unhandled exceptions thrown during invocation. Enabled by default.
    /// </summary>
    public bool EnableDefaultExceptionHandler { get; set; } = true;

    /// <summary>
    /// Configures the command line to write error information to standard error when there are errors parsing command line input. Enabled by default.
    /// </summary>
    public bool EnableParseErrorReporting { get; set; } = true;

    /// <summary>
    /// Configures the application to provide alternative suggestions when a parse error is detected. Disabled by default.
    /// </summary>
    public bool EnableTypoCorrections { get; set; } = false;

    /// <summary>
    /// Enables signaling and handling of process termination (Ctrl+C, SIGINT, SIGTERM) via a <see cref="CancellationToken"/> 
    /// that can be passed to a <see cref="CliAction"/> during invocation.
    /// If not provided, a default timeout of 2 seconds is enforced.
    /// </summary>
    public TimeSpan? ProcessTerminationTimeout { get; set; } = TimeSpan.FromSeconds(2);

    /// <summary>
    /// Response file token replacer, enabled by default.
    /// To disable response files support, this property needs to be set to null.
    /// </summary>
    /// <remarks>
    /// When enabled, any token prefixed with <code>@</code> 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.
    /// </remarks>
    public TryReplaceToken? ResponseFileTokenReplacer { get; set; }

    /// <summary>
    /// Gets the root command.
    /// </summary>
    public CliCommand RootCommand { get; }

    /// <summary>
    /// The standard output. Used by Help and other facilities that write non-error information.
    /// By default it's set to <see cref="Console.Out"/>.
    /// For testing purposes, it can be set to a new instance of <see cref="StringWriter"/>.
    /// If you want to disable the output, please set it to <see cref="TextWriter.Null"/>.
    /// </summary>
    public TextWriter Output { get; set; } = Console.Out;

    /// <summary>
    /// The standard error. Used for printing error information like parse errors.
    /// By default it's set to <see cref="Console.Error"/>.
    /// For testing purposes, it can be set to a new instance of <see cref="StringWriter"/>.
    /// </summary>
    public TextWriter Error { get; set; } = Console.Error;

    /// <summary>
    /// Parses an array strings using the configured <see cref="RootCommand"/>.
    /// </summary>
    /// <param name="args">The string arguments to parse.</param>
    /// <returns>A parse result describing the outcome of the parse operation.</returns>
    public ParseResult Parse(IReadOnlyList<string> args);

    /// <summary>
    /// Parses a command line string value using the configured <see cref="RootCommand"/>.
    /// </summary>
    /// <remarks>The command line string input will be split into tokens as if it had been passed on the command line.</remarks>
    /// <param name="commandLine">A command line string to parse, which can include spaces and quotes equivalent to what can be entered into a terminal.</param>
    /// <returns>A parse result describing the outcome of the parse operation.</returns>
    public ParseResult Parse(string commandLine);
}

/// <summary>
/// Parses command line input.
/// </summary>
public static class CliParser
{
    /// <summary>
    /// Parses a list of arguments.
    /// </summary>
    /// <param name="command">The command to use to parse the command line input.</param>
    /// <param name="args">The string array typically passed to a program's <c>Main</c> method.</param>
    /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
    /// <returns>A <see cref="ParseResult"/> providing details about the parse operation.</returns>
    public static ParseResult Parse(CliCommand command, IReadOnlyList<string> args, CliConfiguration? configuration = null);

    /// <summary>
    /// Parses a command line string.
    /// </summary>
    /// <param name="command">The command to use to parse the command line input.</param>
    /// <param name="commandLine">The complete command line input prior to splitting and tokenization. This input is not typically available when the parser is called from <c>Program.Main</c>. It is primarily used when calculating completions via the <c>dotnet-suggest</c> tool.</param>
    /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
    /// <remarks>The command line string input will be split into tokens as if it had been passed on the command line.</remarks>
    /// <returns>A <see cref="ParseResult"/> providing details about the parse operation.</returns>
    public static ParseResult Parse(CliCommand command, string commandLine, CliConfiguration? configuration = null);

    /// <summary>
    /// Splits a string into a sequence of strings based on whitespace and quotation marks.
    /// </summary>
    /// <param name="commandLine">A command line input string.</param>
    /// <returns>A sequence of strings.</returns>
    public static IEnumerable<string> SplitCommandLine(string commandLine);
}

/// <summary>
/// Describes the results of parsing a command line input based on a specific parser configuration.
/// </summary>
public sealed class ParseResult
{
    /// <summary>
    /// A result indicating the command specified in the command line input.
    /// </summary>
    public CommandResult CommandResult { get; }

    /// <summary>
    /// The configuration used to produce the parse result.
    /// </summary>
    public CliConfiguration Configuration { get; }

    /// <summary>
    /// Gets the root command result.
    /// </summary>
    public CommandResult RootCommandResult { get; }

    /// <summary>
    /// Gets the parse errors found while parsing command line input.
    /// </summary>
    public IReadOnlyList<ParseError> Errors { get; }

    /// <summary>
    /// Gets the tokens identified while parsing command line input.
    /// </summary>
    public IReadOnlyList<CliToken> Tokens { get; }

    /// <summary>
    /// Gets the list of tokens used on the command line that were not matched by the parser.
    /// </summary>
    public string[] UnmatchedTokens { get; }

    /// <summary>
    /// Gets the completion context for the parse result.
    /// </summary>
    public CompletionContext GetCompletionContext();

    /// <summary>
    /// Gets the parsed or default value for the specified argument.
    /// </summary>
    /// <param name="argument">The argument for which to get a value.</param>
    /// <returns>The parsed value or a configured default.</returns>
    public T? GetValue<T>(CliArgument<T> argument);

    /// <summary>
    /// Gets the parsed or default value for the specified option.
    /// </summary>
    /// <param name="option">The option for which to get a value.</param>
    /// <returns>The parsed value or a configured default.</returns>
    public T? GetValue<T>(CliOption<T> option);

    /// <summary>
    /// Gets the parsed or default value for the specified symbol name, in the context of parsed command (not entire symbol tree).
    /// </summary>
    /// <param name="name">The name of the Symbol for which to get a value.</param>
    /// <returns>The parsed value or a configured default.</returns>
    /// <exception cref="InvalidOperationException">Thrown when parsing resulted in parse error(s).</exception>
    /// <exception cref="ArgumentException">Thrown when there was no symbol defined for given name for the parsed command.</exception>
    /// <exception cref="InvalidCastException">Thrown when parsed result can not be casted to <typeparamref name="T"/>.</exception>
    public T? GetValue<T>(string name);
    
    /// <summary>
    /// Gets the result, if any, for the specified argument.
    /// </summary>
    /// <param name="argument">The argument for which to find a result.</param>
    /// <returns>A result for the specified argument, or <see langword="null"/> if it was not provided and no default was configured.</returns>
    public ArgumentResult? FindResultFor(CliArgument argument);

    public CommandResult? FindResultFor(CliCommand command);

    public OptionResult? FindResultFor(CliOption option);

    public DirectiveResult? FindResultFor(CliDirective directive);

    public SymbolResult? FindResultFor(CliSymbol symbol);
}

Validation

public abstract class CliArgument : CliSymbol
{
    /// <summary>
    /// Provides a list of argument validators. Validators can be used
    /// to provide custom errors based on user input.
    /// </summary>
    public List<Action<ArgumentResult>> Validators { get; }
    
    /// <summary>
    /// Configures the argument to accept only the specified values, and to suggest them as command line completions.
    /// </summary>
    /// <param name="values">The values that are allowed for the argument.</param>
    public void AcceptOnlyFromAmong(params string[] values);

    /// <summary>
    /// Configures the argument to accept only values representing legal file paths.
    /// </summary>
    public void AcceptLegalFilePathsOnly();

    /// <summary>
    /// Configures the argument to accept only values representing legal file names.
    /// </summary>
    /// <remarks>A parse error will result, for example, if file path separators are found in the parsed value.</remarks>
    public void AcceptLegalFileNamesOnly();
}

/// <summary>
/// Provides extension methods for <see cref="CliArgument" />.
/// </summary>
public static class ArgumentValidation
{
    /// <summary>
    /// Configures an argument to accept only values corresponding to an existing file.
    /// </summary>
    /// <param name="argument">The argument to configure.</param>
    /// <returns>The configured argument.</returns>
    public static CliArgument<FileInfo> AcceptExistingOnly(this CliArgument<FileInfo> argument);

    public static CliArgument<DirectoryInfo> AcceptExistingOnly(this CliArgument<DirectoryInfo> argument);

    public static CliArgument<FileSystemInfo> AcceptExistingOnly(this CliArgument<FileSystemInfo> argument);

    public static CliArgument<T> AcceptExistingOnly<T>(this CliArgument<T> argument)
        where T : IEnumerable<FileSystemInfo>
}


public abstract class CliOption : CliSymbol
{
    /// <summary>
    /// Validators that will be called when the option is matched by the parser.
    /// </summary>
    public List<Action<OptionResult>> Validators { get; }
    
    /// <summary>
    /// Configures the option to accept only the specified values, and to suggest them as command line completions.
    /// </summary>
    /// <param name="values">The values that are allowed for the option.</param>
    public void AcceptOnlyFromAmong(params string[] values);

    /// <summary>
    /// Configures the option to accept only values representing legal file paths.
    /// </summary>
    public void AcceptLegalFilePathsOnly();

    /// <summary>
    /// Configures the option to accept only values representing legal file names.
    /// </summary>
    /// <remarks>A parse error will result, for example, if file path separators are found in the parsed value.</remarks>
    public void AcceptLegalFileNamesOnly();
}

/// <summary>
/// Provides extension methods for <see cref="CliOption" />.
/// </summary>
public static class OptionValidation
{
    public static CliOption<FileInfo> AcceptExistingOnly(this CliOption<FileInfo> option);
    
    public static CliOption<DirectoryInfo> AcceptExistingOnly(this CliOption<DirectoryInfo> option);

    public static CliOption<FileSystemInfo> AcceptExistingOnly(this CliOption<FileSystemInfo> option);

    public static CliOption<T> AcceptExistingOnly<T>(this CliOption<T> option)
        where T : IEnumerable<FileSystemInfo>
}

public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
    /// <summary>
    /// Validators to the command. Validators can be used
    /// to create custom validation logic.
    /// </summary>
    public List<Action<CommandResult>> Validators { get; }
}

Completions

namespace System.CommandLine.Completions;

/// <summary>
/// Provides details about a command line completion item.
/// </summary>
/// reference: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
public class CompletionItem : IEquatable<CompletionItem>
{
    /// <param name="label">The label value, which is the text displayed to users and, unless <paramref name="insertText"/> is set, is also used to populate the <see cref="InsertText"/> property.</param>
    /// <param name="kind">The kind of completion item.</param>
    /// <param name="sortText">The value used to sort the completion item in a list. If this is not provided, then <paramref name="label"/>  is used.</param>
    /// <param name="insertText">The text to be inserted by this completion item. If this is not provided, then <paramref name="label"/>  is used.</param>
    /// <param name="documentation">Documentation about the completion item.</param>
    /// <param name="detail">Additional details regarding the completion item.</param>
    public CompletionItem(string label, string kind = "Value", string? sortText = null, string? insertText = null, 
        string? documentation = null, string? detail = null);

    /// <summary>
    /// The label value, which is the text displayed to users.
    /// </summary>
    public string Label { get; }

    /// <summary>
    /// The kind of completion item.
    /// </summary>
    public string? Kind { get; }

    /// <summary>
    /// The value used to sort the completion item in a list.
    /// </summary>
    public string SortText { get; }

    /// <summary>
    /// The text to be inserted by this completion item.
    /// </summary>
    public string? InsertText { get; }

    /// <summary>
    /// Documentation about the completion item.
    /// </summary>
    public string? Documentation { get; set; }

    /// <summary>
    /// Additional details regarding the completion item.
    /// </summary>
    public string? Detail { get; }
}

/// <summary>
/// Supports command line completion operations.
/// </summary>
public class CompletionContext
{
    /// The text of the word to be completed, if any.
    public string WordToComplete { get; }

    /// The parse result for which completions are being requested.
    public ParseResult ParseResult { get; }

    /// <summary>
    /// Gets an empty CompletionContext.
    /// </summary>
    /// <remarks>Can be used for testing purposes.</remarks>
    public static CompletionContext Empty { get; }
}

/// <summary>
/// Provides details for calculating completions in the context of complete, unsplit command line text.
/// </summary>
public class TextCompletionContext : CompletionContext
{
    /// <summary>
    /// The position of the cursor within the command line. 
    /// </summary>
    public int CursorPosition { get; }

    /// <summary>
    /// The complete text of the command line prior to splitting, including any additional whitespace.
    /// </summary>
    public string CommandLineText { get; }

    /// <summary>
    /// Creates a new instance of <see cref="TextCompletionContext"/> at the specified cursor position.
    /// </summary>
    /// <param name="position">The cursor position at which completions are calculated.</param>
    public TextCompletionContext AtCursorPosition(int position);
}

public abstract class CliSymbol
{
    /// <summary>
    /// Gets completions for the symbol.
    /// </summary>
    public abstract IEnumerable<CompletionItem> GetCompletions(CompletionContext context);
}

public abstract class CliArgument : CliSymbol
{
    /// <summary>
    /// Gets the list of completion sources for the argument.
    /// </summary>
    public List<Func<CompletionContext, IEnumerable<CompletionItem>>> CompletionSources { get; }
}

public abstract class CliOption : CliSymbol
{
    /// <summary>
    /// Gets the list of completion sources for the option.
    /// </summary>
    public List<Func<CompletionContext, IEnumerable<CompletionItem>>> CompletionSources { get; }
}

public sealed class ParseResult
{
    /// <summary>
    /// Gets completions based on a given parse result.
    /// </summary>
    /// <param name="position">The position at which completions are requested.</param>
    /// <returns>A set of completions for completion.</returns>
    public IEnumerable<CompletionItem> GetCompletions(int? position = null);
}

Execution

/// <summary>
/// Defines the behavior of a symbol.
/// </summary>
public abstract class CliAction
{
    /// <summary>
    /// Performs an action when the associated symbol is invoked on the command line.
    /// </summary>
    /// <param name="parseResult">Provides the parse results.</param>
    /// <returns>A value that can be used as the exit code for the process.</returns>
    public abstract int Invoke(ParseResult parseResult);

    /// <summary>
    /// Performs an action when the associated symbol is invoked on the command line.
    /// </summary>
    /// <param name="parseResult">Provides the parse results.</param>
    /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
    /// <returns>A value that can be used as the exit code for the process.</returns>
    public abstract Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default);
}

public abstract class CliOption : CliSymbol
{
    /// <summary>
    /// Gets or sets the <see cref="CliAction"/> for the Option. The handler represents the action
    /// that will be performed when the Option is invoked.
    /// </summary>
    public virtual CliAction? Action { get; set; }
}

public sealed class HelpAction : CliAction
{
    // it will expose help customization APIs in the near future
}

public class CliDirective : CliSymbol
{
    /// <summary>
    /// Gets or sets the <see cref="CliAction"/> for the Directive. The handler represents the action
    /// that will be performed when the Directive is invoked.
    /// </summary>
    public virtual CliAction? Action { get; set; }
}

public class CommandAction : CliAction
{
}

public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
    /// <summary>
    /// Gets or sets the <see cref="CommandAction"/> for the Command. The handler represents the action
    /// that will be performed when the Command is invoked.
    /// </summary>
    /// <remarks>
    /// <para>Use one of the <see cref="SetAction(Action{ParseResult})" /> overloads to construct a handler.</para>
    /// <para>If the handler is not specified, parser errors will be generated for command line input that
    /// invokes this Command.</para></remarks>
    public CommandAction? Action { get; set; }
    
    /// <summary>
    /// Sets a synchronous action to be run when the command is invoked.
    /// </summary>
    public void SetAction(Action<ParseResult> action);

    /// <summary>
    /// Sets a synchronous action to be run when the command is invoked.
    /// </summary>
    /// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
    public void SetAction(Func<ParseResult, int> action);

    /// <summary>
    /// Sets an asynchronous action to be run when the command is invoked.
    /// </summary>
    public void SetAction(Func<ParseResult, CancellationToken, Task> action);

    /// <summary>
    /// Sets an asynchronous action when the command is invoked.
    /// </summary>
    /// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
    public void SetAction(Func<ParseResult, CancellationToken, Task<int>> action);
}

public sealed class ParseResult
{
    /// <summary>
    /// Gets the <see cref="CliAction"/> for parsed result. The handler represents the action
    /// that will be performed when the parse result is invoked.
    /// </summary>
    public CliAction? Action { get; }

    /// <summary>
    /// Invokes the appropriate command handler for a parsed command line input.
    /// </summary>
    /// <param name="cancellationToken">A token that can be used to cancel an invocation.</param>
    /// <returns>A task whose result can be used as a process exit code.</returns>
    public Task<int> InvokeAsync(CancellationToken cancellationToken = default);

    /// <summary>
    /// Invokes the appropriate command handler for a parsed command line input.
    /// </summary>
    /// <returns>A value that can be used as a process exit code.</returns>
    public int Invoke() => InvocationPipeline.Invoke(this);
}

Help customization

TBA

@adamsitnik adamsitnik added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Mar 30, 2023
@bartonjs
Copy link
Member

bartonjs commented Mar 30, 2023

Video

  • CliSymbol.IsHidden => Hidden (drop the "Is")
  • CliSymbol.Parents uses a yield return iterator, so it's correct to be an IEnumerable instead of a more specific type.
  • CliArgument<T> doesn't yet seem justified as being generic, but the T is promised to be applicable later.
  • CliOption.AppliesToSelfAndChildren => Recursive
  • CliOption.IsRequred = Required
  • CliOption.Aliases is a private custom collection type, so the interface return is correct.
    • We discussed ISet vs ICollection, and feel ISet is not appropriate.
  • CliOption we added ValueType for symmetry with CliArgument
  • CliCommand.Children uses yield return, so IEnumerable is OK
  • CliChildren.Arguments, Options, and Subcommands are all non-public custom collection types, so IList is correct.
  • DiagramDirective: errorExitCode was renamed to parseErrorReturnValue, and made a get/set property instead of a ctor parameter.
  • We discussed the "Cli" prefix. We agree there does need to be some particle (because "Symbol", "Option", "Command", etc are too general and will probably have app collisions). No one thought it was great, but no one had a better name.

The other four phases should be opened as 4 separate issues to be discussed individually.

namespace System.CommandLine;

/// <summary>
/// Defines a named symbol that resides in a hierarchy with parent and child symbols.
/// </summary>
public abstract class CliSymbol
{
    private protected CliSymbol(string name);

    /// <summary>
    /// Gets or sets the description of the symbol.
    /// </summary>
    public string? Description { get; set; }

    /// <summary>
    /// Gets the name of the symbol.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the symbol is hidden.
    /// </summary>
    public bool Hidden { get; set; }

    /// <summary>
    /// Gets the parent symbols.
    /// </summary>
    public IEnumerable<CliSymbol> Parents { get; }
}

/// <summary>
/// A symbol defining a value that can be passed on the command line to a <see cref="CliCommand">command</see> or <see cref="CliOption">option</see>.
/// </summary>
public abstract class CliArgument : CliSymbol
{
    private protected CliArgument(string name);

    /// <summary>
    /// The name used in help output to describe the argument. 
    /// </summary>
    /// Example: `dotnet build --help`
    /// Arguments:
    /// <PROJECT | SOLUTION>  The project or solution file to operate on.
    /// "PROJECT | SOLUTION" is the HelpName here
    public string? HelpName { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.
    /// </summary>
    public abstract Type ValueType { get; }
}

public class CliArgument<T> : CliArgument
{
    /// <summary>
    /// Initializes a new instance of the Argument class.
    /// </summary>
    /// <param name="name">The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.</param>>
    public CliArgument(string name);

    /// <inheritdoc />
    public override Type ValueType => typeof(T);
}

/// <summary>
/// A symbol defining a named parameter and a value for that parameter. 
/// </summary>
public abstract class CliOption : CliSymbol
{
    private protected CliArgument(string name);

    /// <summary>
    /// Gets or sets the name of the Option when displayed in help.
    /// </summary>
    /// <value>
    /// The name of the option when displayed in help.
    /// </value>
    /// <remarks>Useful for localization, as it's not used for actual parsing.</remarks>
    public string? HelpName { get; set; }

    /// <summary>
    /// When set to true, this option will be applied to the command and recursively to subcommands.
    /// It will not apply to parent commands.
    /// </summary>
    public bool Recursive { get; set; }

    /// <summary>
    /// Indicates whether the option is required when its parent command is invoked.
    /// </summary>
    /// <remarks>When an option is required and its parent command is invoked without it, an error results.</remarks>
    public bool Required { get; set; }

    /// <summary>
    /// Gets the unique set of strings that can be used on the command line to specify the Option.
    /// </summary>
    /// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Option.</remarks>
    public ICollection<string> Aliases { get; }

    /// <summary>
    /// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.
    /// </summary>
    public abstract Type ValueType { get; }
}

public class CliOption<T> : CliOption
{
    /// <summary>
    /// Initializes a new instance of the Option class.
    /// </summary>
    /// <param name="name">The name of the option. It's used for parsing, displaying Help and creating parse errors.</param>>
    /// <param name="aliases">Optional aliases. Used for parsing, suggestions and displayed in Help.</param>
    public CliOption(string name, params string[] aliases);

    /// <inheritdoc />
    public override Type ValueType => typeof(T);
}

/// <summary>
/// Represents a specific action that the application performs.
/// </summary>
/// <remarks>
/// Use the Command object for actions that correspond to a specific string (the command name). See
/// <see cref="RootCommand"/> for simple applications that only have one action. For example, <c>dotnet run</c>
/// uses <c>run</c> as the command.
/// </remarks>
public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
    /// <summary>
    /// Initializes a new instance of the Command class.
    /// </summary>
    /// <param name="name">The name of the command.</param>
    /// <param name="description">The description of the command, shown in help.</param>
    public CliCommand(string name, string? description = null);

    /// <summary>
    /// Gets the child symbols.
    /// </summary>
    public IEnumerable<CliSymbol> Children { get; }

    /// <summary>
    /// Represents all of the arguments for the command.
    /// </summary>
    public IList<CliArgument> Arguments { get; }

    /// <summary>
    /// Represents all of the options for the command, including global options that have been applied to any of the command's ancestors.
    /// </summary>
    public IList<CliOption> Options { get; }

    /// <summary>
    /// Represents all of the subcommands for the command.
    /// </summary>
    public IList<CliCommand> Subcommands { get; }

    /// <summary>
    /// Gets the unique set of strings that can be used on the command line to specify the command.
    /// </summary>
    /// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Command.</remarks>
    public ICollection<string> Aliases { get; }

    /// <summary>
    /// Adds a <see cref="CliSymbol"/> to the command.
    /// </summary>
    /// <param name="symbol">The symbol to add to the command.</param>
    [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# duck typing
    public void Add(CliSymbol symbol);

    /// <summary>
    /// Represents all of the symbols for the command.
    /// </summary>
    public IEnumerator<CliSymbol> GetEnumerator() => Children.GetEnumerator();

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

/// <summary>
/// The purpose of directives is to provide cross-cutting functionality that can apply across command-line apps.
/// Because directives are syntactically distinct from the app's own syntax, they can provide functionality that applies across apps.
/// 
/// A directive must conform to the following syntax rules:
/// * It's a token on the command line that comes after the app's name but before any subcommands or options.
/// * It's enclosed in square brackets.
/// * It doesn't contain spaces.
/// </summary>
public class CliDirective : CliSymbol
{
    /// <summary>
    /// Initializes a new instance of the Directive class.
    /// </summary>
    /// <param name="name">The name of the directive. It can't contain whitespaces.</param>
    public CliDirective(string name);
}

/// <summary>
/// Enables the use of the <c>[env:key=value]</c> directive, allowing environment variables to be set from the command line during invocation.
/// </summary>
public sealed class EnvironmentVariablesDirective : CliDirective
{
    public EnvironmentVariablesDirective();
}

/// <summary>
/// Enables the use of the <c>[suggest]</c> directive which when specified in command line input short circuits normal command handling and writes a newline-delimited list of suggestions suitable for use by most shells to provide command line completions.
/// </summary>
/// <remarks>The <c>dotnet-suggest</c> tool requires the suggest directive to be enabled for an application to provide completions.</remarks>
public sealed class SuggestDirective : CliDirective
{
    public SuggestDirective();
}

/// <summary>
/// Enables the use of the <c>[diagram]</c> directive, which when specified on the command line will short 
/// circuit normal command handling and display a diagram explaining the parse result for the command line input.
/// </summary>
/// Example: dotnet [diagram] build -c Release -f net7.0
/// Output: [ dotnet [ build [ -c <Release> ] [ -f <net7.0> ] ] ]
public sealed class DiagramDirective : CliDirective
{
    /// <param name="errorExitCode">If the parse result contains errors, this exit code will be used when the process exits.</param>
    public DiagramDirective();
    
    public int ParseErrorReturnValue { get; set; } = DefaultErrorExitCode);
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Mar 30, 2023
@carlreinke
Copy link
Contributor

CliOption.AppliesToSelfAndChildren => Recursive

When something is optionally passed from parent to children, I'd expect the flag to be called called Inherited.

@alexrp
Copy link
Contributor

alexrp commented Apr 23, 2023

Just to be clear, are the new APIs still aiming to avoid a hard dependency on System.Console?

@adamsitnik
Copy link
Member

Just to be clear, are the new APIs still aiming to avoid a hard dependency on System.Console?

The new APIs have a dependency on System.Console, as the default Output and Error are Console.Out and Console.Error (they are being used to print help, parsing errors etc). But they can be set to any other TextWriter

https://github.com/dotnet/command-line-api/blob/87704ce036fb23a4174b8290f249706aa35ab255/src/System.CommandLine/CliConfiguration.cs#L98-L119

@adamsitnik adamsitnik modified the milestones: 8.0.0, 9.0.0 Jul 27, 2023
adamsitnik added a commit to adamsitnik/scl_prototype that referenced this issue Oct 18, 2023
diff with what was officially approved in dotnet/runtime#68578 (comment):
- CliSymbol, CliArgument, CliOption and CliCommand moved to new System.CommandLine.Parsing library
- abstract types moved from System.CommandLine to System.CommandLine.Symbols namespace as they should be rarely used
- HelpOption and VersionOption moved to System.CommandLine.Help library
- CliRootCommand moved to main System.CommandLine package
- CliSymbol.Description made virtual, to allow for customization like loading lazily from resources
- CliSymbol extended with Terminating property that let's the parser know that given symbol terminates parsing (example: help)
- CliSymbol.HelpName removed, as symbols are now unaware of help
- CliCOmmand.Add and CliCommand.Children made virtual to allow for CliRootCommand extend with Directives
@jeffhandley
Copy link
Member

Closing this issue per dotnet/command-line-api#2338.

@github-actions github-actions bot locked and limited conversation to collaborators Apr 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Console
Projects
None yet
Development

No branches or pull requests