Skip to content

[API proposal]: System.CommandLine APIs #68578

Closed

Description

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();
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions