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

More design questions #2046

Closed
adamsitnik opened this issue Feb 14, 2023 · 39 comments · Fixed by #2107
Closed

More design questions #2046

adamsitnik opened this issue Feb 14, 2023 · 39 comments · Fixed by #2107
Labels
needs discussion Further discussion required
Milestone

Comments

@adamsitnik
Copy link
Member

We are soon going to have another internal round of design discussion for System.CommandLine. I am sharing the list of open questions here so everyone can share their feedback.

Symbols

Symbol

Description

Symbol.Description does not need to be virtual: #2045

Every symbol can provide the description in the ctor.

Argument

HelpName

When Argument is parsed, we use its position rather than name. But when we want to refer to it, for example when displaying help, it may need a name.

Currently the type has both Name and HelpName. Why does Argument need both?

Arity

Can anyone provide a better name?

Parse(string), Parse(string[])

Both methods are used only for testing purposes in System.CommandLine. They don't provide any possibility to customize the configuraiton used for parsing. They can be easily replaced with one liner:

new CommandLineBuilder(new Command() { argument }).UseDefaults().Build().Parse($input);

I believe that we should remove them or made them internal.

Validators

Currently a validator is an Action<ArgumentResult>. What I don't like about this design is the fact that it allows for modificaitons of the ArgumentResult.

Example:

argument.Validators.Add(argumentResult => argumentResult.OnlyTake(3));

It's hard to propose a better design, as some validators need more than just SymbolResult.Tokens. Example: validator may want to call GetValueOrDefault to validate parsed value.

Argument

ctors

Argument<T> provides multiple ways of setting default value: as T, Func<T> and Func<ArgumentResult, T>

public Argument(Func<T> defaultValueFactory)
    
public Argument(
    string name,
    T defaultValue,
    string? description = null)

public Argument(
    string? name,
    Func<ArgumentResult, T> parse,
    bool isDefault = false,
    string? description = null)

What I don't like is the fact that the last ctor requires the user to define whether given Func<ArgumentResult, T> is a default value provider or a custom parser. It's confusing.

My understanding is that we need custom parser for complex types and default values for all kinds of types. IMO the latter is way more common than the first.

Do we really need a default value provider that uses ArgumentResult as an input?
Do we really need the default value to be lazily provided?

How about having just one ctor:

public Argument(
    string? name = null,
    string? description = null
    T? defaultValue = null,
    Func<ArgumentResult, T> customParser = null)

SetDefaultValue, SetDefaultValueFactory

Same as above, there are 3 different ways of setting default value:

public void SetDefaultValue(T value)
public void SetDefaultValueFactory(Func<T> defaultValueFactory)
public void SetDefaultValueFactory(Func<ArgumentResult, T> defaultValueFactory)

I would stick with two:

public void SetDefaultValue(T value)
public void SetCustomParser(Func<ArgumentResult, T> customParser)

AcceptLegalFilePathsOnly, AcceptLegalFileNamesOnly

These instance methods are usefull only for argument of string, FileInfo and arrays of these two.

Currently they can be used for all kinds of T:

Argument<int> portNumber = new ();
portNumber.AcceptLegalFilePathsOnly();

Does anyone have a better idea how to make sure they don't get suggested/used for T other than the ones it makes sense for?

We could of course introduce them as extension method of Argument<string>, Argument<string[]>, Argument<FileInfo>, Argument<FileInfo[]>, but I doubt it would be accepted by BCL review board.

IdentifierSymbol

From my point of view the existence of this type is an implementation detail (it's a Symbol with Aliases).

I need to discuss this case with Jeremy.

Option

ArgumentHelpName

Similar to Argument.Name: the users have the possibility to set Name for Option. Do we really need this property? We use aliases for parsing and it seems like Name is exactly what we need for help.

IsGlobal

This property is currently internal, but we should expose it.

We need a better name. Ideas?

Parse(string), Parse(string[])

Same as Arugment.Parse: do we really need these methods to be public?

Option

ctors

Same as for Argument<T>: we need a better way to distinguish default value provider and custom parser.

Command

Command.Children

Command exposes Argument, Options, and Subcommands properties.

It also implements IEnumerable<Symbol> and Add(Symbol) to support a very common duck typing C# syntax:

Command cmd = new ()
{
    new Argument<int>() // uses duck typing and calls Command.Add(Symbol).
};

Do we really need to expose IEnumerable<Symbol> Children on top of that? It seems redundant to me.

AddGlobalOption

If we made Option.IsGlobal public, we can remove this method, so users can do:

command.Options.Add(new Option<bool>("some") { IsGlobal = true });

TreatUnmatchedTokensAsErrors

Does it make sense to make this setting specific to given command? Should it become a part of CommandLineConfiguration?

Parse

Command has no Parse instance methods, but it does have an extension method.

IMO we should make it an instance method with optional CommandLineConfiguration argument to make it easier to discover the configurability of parsing:

ParseResult Parse(CommandLineConfiguration? configuration = null)

RootCommand

Currently it exposes only two public methods:

public static string ExecutablePath => Environment.GetCommandLineArgs()[0];

public static string ExecutableName => Path.GetFileNameWithoutExtension(ExecutablePath).Replace(" ", "");

This information can be easily obtained by System.CommandLine users (by using System.Environment class).

Currently the only difference with Command is that it does not allow for setting custom alias (which should be ultra rare). Could this be achieved without a dedicated RootCommand type? I want to reduce the number of concepts the users need to get familiar with. Better perf (fewer types to load and JIT) is just a side effect of that,

Parsing

CommandLineConfiguration

This type provides multiple internal properites and some public properties. All of them are readonly. Should we make them mutable to avoid the need of having a builder type?

EnableEnvironmentVariableDirective, EnableSuggestDirective, EnableDirectives

If we expose all the internal properties, are we going to introduce a new property for every new directive?

What are the alternatives other than creating dedicated directive types and adding Command.Directives property?

EnableTokenReplacement

Why is it enabled by default?

TokenReplacer

Why do we need custom token replacers? What do users customize other than supporting comments in the response files?

Perhaps we should expose an enumeration and provide default implementation?

enum ResponseFiles
{
    Disabled,
    Enabled,
    SpecificStandardName1,
    SpecificStandardName2,
    
}

config.ResponseFiles = ResponseFiles.Disabled;

What I don't like about the current design is that InvokeAsync performs parsing which can perform a blocking call when reading the response file:

var lines = File.ReadAllLines(filePath);

If we just provide an enumeration, it would be easier to do the right thing (call async APIs for async invocation, pass CancellationToken etc).

LocalizationResources

In BCL, System.* libraries that need localization provide their own set of resource files, configure automation with https://github.com/dotnet/arcade/blob/main/Documentation/OneLocBuild.md and don't expose a possibility to customize the transaltions. This type needs to be removed / made internal.

Please see #2041 for more details.

CommandLineBuilder

The type name is confusing to me. It builds a parser and configuration and I would expect it to be called CommandLineConfigurationBuilder. But perhaps we can remove it if we make CommandLineConfiguration mutable?

It defines almost no public methods, but CommandLineBuilderExtensions provides plenty of its extensions.

Properties are not always enough

Not every configuration can be expressed with a single property. Some methods allow for additional customization.

For example UseParseErrorReporting allows for enabling the parse error reporting and setting a custom exit code for parsing errors:

public static CommandLineBuilder UseParseErrorReporting(
    this CommandLineBuilder builder,
    int errorExitCode = 1)

RegisterWithDotnetSuggest

The current design has plenty of flaws. One of them is major perf hit for default config (creating files and starting new process to register).

We most likely need a separate meeting dedicated to dotnet suggest. Who should be invited?

UseDefaults

Before we find a proper solution for RegisterWithDotnetSuggest, can we disable it by default?

UseExceptionHandler

Curent API:

public static CommandLineBuilder UseExceptionHandler(
    this CommandLineBuilder builder,
    Action<Exception, InvocationContext>? onException = null,
    int? errorExitCode = null)

In my opinion exception handler should always return an int. It would make the config simpler and easier to discover the possibility to set custom exit code.

public static CommandLineBuilder UseExceptionHandler(
    this CommandLineBuilder builder,
    Func<Exception, InvocationContext, int>? onException = null);

AddMiddleware

Middleware is VERY powerfull. After #2043 we are close to having 0 middleware by default (it's expensive).

I need to make a research and see why our users provide custom middleware. Just to make sure it's not just a mechanism for workarounding our limitations like missing configuraiton etc.

UseHelp

I've not analysed this part of S.CL yet. I will have comments in the near future.

Parser

First of all I believe that this type is simply hard to discover. The only state it has is CommandLineConfiguration. To make CommandLineConfiguration more discoverable I would make Parser static with just one method:

static class Parser
{
    static PareResult Parse(string[] args, CommandLineConfiguration configuration = default);
}

We also need to rename it to CliParser to avoid conflicts with dozen other parsers ;)

ParseResult

Directives

Currently the only way for the users to implement custom directives is enabling directives parsing in the config and using this property to get parsed output.

The problem is that if the users want to perform any custom action based on the directive input like setting custom culture: [culture pl] they need to use Middleware (expensive).

Should we expose a dedicated Directive symbol type?

class Directive : Symbol
{
    Directive(string name, Action<string> onParsed);
}

UnmatchedTokens

The Tokens property returns a collection of Token type. UnmatchedTokens returns a collection of strings. Why don't we expose full information?

GetValue

Do we need both the generic and non-generic overloads?

object? GetValue(Option option) 
T? GetValue<T>(Option<T> option)
object? GetValue(Argument argument)
T GetValue<T>(Argument<T> argument)

SymbolResult

SymbolResult and it's entire family LGTM after recent changes.

@jonsequitur jonsequitur added the needs discussion Further discussion required label Feb 14, 2023
@jonsequitur jonsequitur added this to the 2.0 GA milestone Feb 14, 2023
@KalleOlaviNiemitalo
Copy link

Do we really need a default value provider that uses ArgumentResult as an input?
Do we really need the default value to be lazily provided?

I need access to the values of other command-line options in completions and validation, but apparently not in parsing and default values.

The main scenario for default values would be something like --target in MSBuild, which defaults to whatever is specified in the DefaultTargets attribute in the project file that can be specified as a command-line argument; but this can also be modelled as having null as the default and translating that in the command handler after parsing.

@KalleOlaviNiemitalo
Copy link

Currently the type has both Name and HelpName. Why does Argument need both?

My understanding is that Argument.Name is culture-invariant for binding against names from reflection, and Argument.HelpName is localised for help.

But, perhaps it would be better to have a string BindingName property dedicated to binding, not corresponding to a parameter of the constructor.

@KalleOlaviNiemitalo
Copy link

If we made Option.IsGlobal public, we can remove this method

Yes please. The current AddGlobalOption method mutates the Option by setting IsGlobal so it is not possible to add the same Option as a global option of one command tree and as a local option in another command. Making IsGlobal public would make this obvious rather than a surprise.

@KalleOlaviNiemitalo
Copy link

Directive(string name, Action<string> onParsed);

No, this presupposes that invocation is to continue after the directive. Which is not true for the [parse] and [suggest] directives and so likely would not be true for application-defined directives either.

@KalleOlaviNiemitalo
Copy link

Do we need both the generic and non-generic overloads [of ParseResult.GetValue]?

The generic methods are nice to use with type deduction; please keep them.

The non-generic methods might be useful for some reflection scenario where the caller does not know the type at compile time. However, if they were removed, then those applications could still call FindResultFor(argument_or_option)?.GetValueOrDefault<object?>() for the same effect. This would not convert the argument but then again the current non-generic methods don't do that either. So I think they are OK to remove.

@karlra
Copy link

karlra commented Feb 14, 2023

Since there was a request for feedback on Twitter, here it is. I am using this library in production and while it's great and I am sure it is very powerful and can do a million things, in 99% of cases you just want to get the damn command line values, no fancy stuff. Maybe I am misunderstanding the library somehow but when I implemented it, what felt like it should be a simple task was not straightforward and that someone forgot about the most common scenario while designing for very complex scenarios.

It took quite some doing and a lot of code (that looks intimidating to a newbie) to understand how to arrive at a simple GetParsedValue< T >(paramName) way of using the library. It involved setting up a custom handler (a method that has 20 overloads), then use an "InvocationContext", whatever that is, to get to the actual parameters. If there is an easier way to achieve this way of using the library, it is sure hidden well.

Also, just a personal opinion, but the suggested way in the docs to bind parameters with SetHandler< T1, T2, T3.... >, in the beta version, supports just 8 parameters, and what happens when you need more than whatever the arbitrary limit is? Total rewrite!

Since a command line invocation is by definition a one-time occurrence, why isn't there a top-level, quick, discoverable way to just get a parsed parameter value by name, like System.CommandLine.Current.GetValue< T >(paramName)?

@patriksvensson
Copy link

Instead of Arity, perhaps Ordinal or Index is a better name?

@jonsequitur
Copy link
Contributor

this can also be modelled as having null as the default and translating that in the command handler after parsing.

This is certainly something people can do, but the default is then unknowable without actually running the command. Experiences like validation, help messages, dry runs, completions, etc. are stronger if this is modeled in the parser layer.

@jonsequitur
Copy link
Contributor

My understanding is that Argument.Name is culture-invariant for binding against names from reflection

This was the original intent but this implementation is no longer used in the core library.

@jonsequitur
Copy link
Contributor

Should we expose a dedicated Directive symbol type?

I would tend to say yes but for invocation, use a common approach that could also potentially be applied to certain options that have "command-like" behaviors, such as --version and --help. Similar to directives like [parse] and [suggest], these indicate an alternative behavior where the rest of the command line input is still significant. We should assume these handlers need to access the InvocationContext just like command handlers do.

@KalleOlaviNiemitalo
Copy link

Instead of Arity, perhaps Ordinal or Index is a better name?

Ordinal and cardinal numbers… Cardinality?

@jonsequitur
Copy link
Contributor

Also, just a personal opinion, but the suggested way in the docs to bind parameters with SetHandler< T1, T2, T3.... >, in the beta version, supports just 8 parameters, and what happens when you need more than whatever the arbitrary limit is? Total rewrite!

Many people share this opinion. We're going to remove those overloads and provide a source generator-based approach to simplify users' code. In the meantime, if you choose the InvocationContext overload, you can parse as many values as you like using InvocationContext.GetValue.

@jonsequitur
Copy link
Contributor

jonsequitur commented Feb 15, 2023

Instead of Arity, perhaps Ordinal or Index is a better name?

Ordinal and cardinal numbers… Cardinality?

I think the concern with "arity" isn't that it's incorrect, but that it's not a word most people understand. Is there a simpler term we could use, using more commonly-known words? Something like NumberOfOccurrences or AllowedRepetitions?

It's also important not to confuse people with how this relates to the parsed value. It's possible to allow a single occurrence but parse it as an array, e.g. parsing 1,2,3,4,5 as a single token:

new Argument<int[]>(parse: r => r.Tokens.Single().Value.Split(',').Select(int.Parse).ToArray()) 
{ 
    Arity = ArgumentArity.ExactlyOne 
} 

It's also possible to allow many occurrences but parse them into a scalar value, e.g. parsing a a a as 3. (This one is a common enough convention for flags that it's been requested a few times as a built-in feature).

new Argument<int>(parse: r => r.Tokens.Count) 
{ 
    Arity = ArgumentArity.OneOrMore 
} 

@patriksvensson
Copy link

Yes, Arity is correct, but the reason I suggested Ordinal is that it is used for the same thing in System.Data.

@ian-buse
Copy link

ian-buse commented Feb 15, 2023

What about something like Quantity or Size instead of Arity? I think these would still get the point across, while remaining concise and being more recognizable.

@jonsequitur
Copy link
Contributor

We most likely need a separate meeting dedicated to dotnet suggest. Who should be invited?

@SteveL-MSFT

@jonsequitur
Copy link
Contributor

The Tokens property returns a collection of Token type. UnmatchedTokens returns a collection of strings. Why don't we expose full information?

If they're not matched, then the token type is likely misleading.

@tbolon
Copy link

tbolon commented Feb 15, 2023

I have recently created a small cmdline app using System.CommandLine and I must admit it was not really fun to write.

  • Adding more and more options as the development continue required me to switch to the non generic SetHandler method, having to rewrite most of my code (already raised here, seems to be fixed).
  • Before switching to the non-generic SetHandler method for the rootCommand, adding parameters was really error prone when most of the parameters have the same type: you must be sure that the lambda and the parameters are in the same order. There is no help.
  • I found the "ParseResult" name unintuitive, I could not discover how to to read option values and arguments quickly just by browsing the InvocationContext. Maybe there should be shortcuts for common cases, where you could discover the API by just browsing the InvocationContext, and hide more advanced features in nested properties? Or why not reversing the call: communityArgument.GetValue(context) (not ideal because it capture variable)? Edit: it's fixed in Rename members to GetValue #1787 👍
  • I discovered the ability to add arguments/options to a command using the collection initializer by reading this message.
  • I tend to prefer fluent initializations, for example having AddOption() with a lot of overloads, returning the created option. But I get that it's really subjective, and that it does not handle the case of creating an option foremost that will be available only on subcommands (you still have to declare the option separately or store the fist subcommand AddOption() returned in a variable used in the second subcommand AddOption()
  • My code is not really compact for this simple case, the options are referenced three times : declaration, added to the command, used to retrieve value.

In general, I found the library not very friendly to "just create a small command-line app", where I would expect a simple experience with an easy to discover happy path.

I know that this library also handle very complex cases, and that you could basically recreate the git command-line this way, but I was hoping there was a way to still be declarative (no magic) and compact / easy to learn (maybe think asp.net core minimal api?)

PS: I suppose there was certainly other ways to make this code more compact or pleasant to read. Feel free to suggest.

I have also created an alternative implementation with some extensions here to illustrate what could be done.

I also know that these suggestions are certainly posted too late, but anyway, in case it could help.
And thanks for this wonderful lib!

@KalleOlaviNiemitalo
Copy link

I found the "ParseResult" name unintuitive, I could not discover how to to read option values and arguments quickly just by browsing the InvocationContext.

#1787 added InvocationContext.GetValue methods, which should be easier to discover.

@adamsitnik
Copy link
Member Author

why isn't there a top-level, quick, discoverable way to just get a parsed parameter value by name, like System.CommandLine.Current.GetValue< T >(paramName)?

They are not too late, we are in a middle of re-design right now.

@adamsitnik
Copy link
Member Author

adamsitnik commented Feb 15, 2023

Execution

InvocationContext

GetCancellationToken()

CancellationToken should be a mandatory argument for every async handler, so the compiler can warn the users when they don't propagate it to async methods.

#2044

LinkToken

There is no need to link the tokens, #2044 removes this method (and keeps things simple).

Once it's removed, the InvocationContext type no longer needs to be disposable.

ExitCode

Similarly to CancellationToken I believe that each handler should just return an integer.

Console

We can't introduce this abstraction right now.

BindingContext

I know that DI can be very useful, but IMO S.CL should not be allowing its users to register new services in its own container. It's just not the scope of this library and I don't really see a good justification for that.

The best we can do is extend configuration with IServiceProvider? and let users provide their own DI container when the configuration is built. Then expose configuration in InvocationContext, so they can easily access their DI container in handler. Users have also asked to make it accessible in Symbol.GetCompletions which currently has the access only to CompletionContext which exposes ParseResult (#2036)

HelpBuilder

We allow the users to customize help in CommandLineBuilder. Once --help is parsed, HelpOption (an internal type) takes care of using the provided HelpBuilder to display help.
I don't see why we should expose it here. Am I missing something?

ParseResult

This is the most important thing that has all we need. If we remove the properties mentioned above, InvocationContext is basically just a ParseResult.

We should consider removing the whole type and making ParseResult the only input for execution (it has parsed data + config).

ParserExtensions

This type creates a feeling that Parser is capable of exucuting the code, while IMO it's job should be just parsing.
We have nice separation of concerns, but IMO the extension methods provided by ParserExtensions diminish that.

Parser parser = new CommandLineBuilder(command).Build();;
parser.InvokeAsync(args);

public static class ParserExtensions
{
    public static int Invoke(
                this Parser parser,
                string[] args,
                IConsole? console = null) =>
                parser.Parse(args).Invoke(console);
}

Once we make it clear that these are two separate things, it's easier to show that parsing is synchronous, while execution can be both sync and async.

IMO we should distinguish 3 steps:

  1. Creating the configuration (optional)
  2. Parsing (mandatory)
  3. Execution (optional, as user might decide to do nothing when there are parse errors etc).
CommandLineConfiguration config = new CommandLineConfigurationBuilder()
    .EnableDirectives()
    .EnablePosixBundling()
    .UseVersionOption()
    .UseHelp()
    .ToConfig();

// I am not sure if we need Parser, we can just use command.Parse.
// It's just easy to discover (I've built a command, now I just use it).
// And it also does not require us to provide Executor for execution.
ParseResult parseResult = rootCommand.Parse(args, config);

int exitCode = await parseResult.InvokeAsync(); // invokes the handler

As I wrote creating config should be optional:

ParseResult parseResult = rootCommand.Parse(args); // uses default config

int exitCode = await parseResult.InvokeAsync();

Handler

Our user study shows that our users are confused with so many SetHandler overloads.

During a design session with @jonsequitur we came to the conclusion that we should most likely have only one SetHandler method.

Since we do want to support both sync and async execution, we need two. Based on what I wrote above, each handler should accept ParseResul (not InvocationContext) and return an integer.
The async handler should require a CancellationToken. To follow the BCL rules, we should not introduce an extension method for a type that we own so it should be just an instance method for Command:

public class Command
{
    public void SetHandler(Func<ParseResult, CancellationToken, Task<int>> asyncHandler);
    public void SetHandler(Func<ParseResult, int> syncHandler);
}

By doing that we could remove ICommandHandler interface and it's implementations.

ParseResult.GetValue(alias)

Another common feedback is lack of ParseResult.GetValue<T>(string nameOrAlias) method, that would not require the users to store the references to options/arguments.

why isn't there a top-level, quick, discoverable way to just get a parsed parameter value by name, like System.CommandLine.Current.GetValue< T >(paramName)?

We definitely want to add it. The only concerns I have is argument name (for arguments it can be only name, for options a name or alias) and duplicate aliases handling.

class ParseResult
{
    T? GetValue(string nameOrAlias);
}

All of that would allow us to turn the following sample:

class Program
{
    static int Main(string[] args)
    {
        Option<bool> boolOption = new(new[] { "--flag", "-b" }, "Bool option");
        Option<string> stringOption = new(new[] { "--text", "-s" }, "String option");

        RootCommand command = new()
        {
            boolOption,
            stringOption
        };

        command.SetHandler((invocationContext) =>
        {
            Console.WriteLine($"Bool option: {invocationContext.GetValue(boolOption)}");
            Console.WriteLine($"String option: {invocationContext.GetValue(stringOption)}");

            invocationContext.ExitCode = 0;
        });

        Parser parser = new CommandLineBuilder(command).Build();
        return parser.Invoke(args);
    }
}

into:

class Program
{
    static int Main(string[] args)
    {
        RootCommand command = new()
        {
            new Option<bool>(new[] { "--flag", "-b" }, "Bool option"),
            new Option<string>(new[] { "--text", "-s" }, "String option")
        };

        command.SetHandler(parseResult =>
        {
            Console.WriteLine($"Bool option: {parseResult.GetValue<bool>("--flag")}");
            Console.WriteLine($"String option: {parseResult.GetValue<string>("--text")}");

            return 0;
        });

        ParseResult parseResult = command.Parse(args);
        return parseResult.Invoke();
    }
}

If we expose a possibility to set handler in Command ctor it could be even:

class Program
{
    static int Main(string[] args)
        => new Command(Handler)
           {
               new Option<bool>(new[] { "--flag", "-b" }, "Bool option"),
               new Option<string>(new[] { "--text", "-s" }, "String option")
           }
           .Parse(args)
           .Invoke();

    static int Handler(ParseResult parseResult)
    {
        Console.WriteLine($"Bool option: {parseResult.GetValue<bool>("--flag")}");
        Console.WriteLine($"String option: {parseResult.GetValue<string>("--text")}");

        return 0;
    }
}

And with the upcoming source-generator based solution:

class Program
{
    static int Main(bool flag, string text)
    {
        Console.WriteLine($"Bool option: {flag}");
        Console.WriteLine($"String option: {text}");

        return 0;
    }
}

@KathleenDollard
Copy link
Contributor

Arity

One of the things we most need help on is a new name for Arity. What about splitting it into two values. What would their names be?

Validators

How bad would it be if someone changed the argument result?

Argument...SetDefaultValue

I agree we could simplify some things on Argument creation. But it important to maintain a way to have the default depend on the state of the world at invocation. The common case is DateTime.Today.AddDays(3). This probably does not need a factory. The next is relative to another arguments value. This probably does need a factory.

AcceptLegalFilePathsOnly...

The problem with type specific validators comes from the fact we treat that and DirectoryInfo complex types specially in the core. Other special types are likely to be recognized only in higher layers.

Identifier symbol

I do think we need a name for commands and options.

TreatUnmatchedTokenAsErrors

TreatUnmatchedTokenAsErrors needs to be command speciifc. For example, the .NET CLI has this false only for selected commands.

Parse

I like the suggestion of the instance parse with configuration.

RootCommand

Two points on RootCommand. We have limited user studies so far, but some signal that devs think differently about the root-the entry point. I would like to explore this before changing.

The other is that I would like to continue to have helper methods for ExecutablePath and ExecutableName. These may be short, but they are not obvious. We could consider putting this on command and then looking up the tree and reporting the root.

EnableEnvironmentDirectives etc.

The "Enable..." stuff should be replaced with the new executable approach.

LocalizationResources

Localization resources: We need some mechanism for users to customize the validation messages. Other messages are cool to lock. If this includes validation messages, lets consider further. The issue is that this would interleave with user's mesages, so maybe allow the message to change, and by implication the localization.

CommandLineBuilder

I think we should rethink CommandLineBuilder with the new execution approach and configuration.

...middleware...

Agree that we need to see if people still need middleware. I think we can accomodate this differently

Help

I think we have to postpone a discussion on Help. It is a large discussion. I would like to simplify the core to just a super simple API and move Help into a separate package. There is too much opinion in what we have and I am worried that the changing the display will be considered breaking.

Parser

One of our user studies perceived the root as the parser. A thing which contained commands, and commands were things typed after the executable name.

Directives

Directives are likely to be caught in the execuation net

@KathleenDollard
Copy link
Contributor

SetHandler

Would it be an error to supply both a sync and an async handler. That might be good but it would take us from requiring both to allowing only one. Might be appropriate, but lets consider whether there are cases where people may have implemented both.

Parseresult.GetValue(alias)

I believe we should keep a name for all symbols (including directives if they become symbols). As this question brings up, we need to be able to find them in code.

@elgonzo
Copy link
Contributor

elgonzo commented Feb 18, 2023

With regard to argument arity/cardinality:

Arguments with a Nullable<T> as value type, like Argument<int?>, for example are optional by default, having the arity {0,1}.
However, this currently does not apply to nullable reference types.

This leads to the in my opinion akward situation regarding code readability/expressivity where for example Argument<int?> would behave differently (the argument being optional, thus not generating an error when omitted in the CLI invocation) than Argument<string?> (the argument being not optional, causing an error when omitted in the CLI invocation), despite the apparent semantics (and presumably the coder's intent) regarding optionality being the same for both Argument<int?> and Argument<string?>.

My apologies if this is on the radar of team already and has been mentioned/discussed elsewhere.

@elgonzo
Copy link
Contributor

elgonzo commented Feb 18, 2023

Instead of Arity, perhaps Ordinal or Index is a better name?

Ordinal and cardinal numbers… Cardinality?

I would prefer Cardinality over either "Ordinal" or "Index". Cardinal numbers describe the size of sets or something, so there is a good overlap with Arity here (the min/max size of the value set of an option/argument).

Ordinal and Index are both wrong. "Ordinal` is related to order/ranking, not to the size/amount of something. "Index" is more a position/pointer/key, and also not so much a size/amount of something.

Size and Quantity are also very good choices. Personally, i would prefer Arity or Cardinality, but in terms of familiarity the terms Size and Quantity beat either of them.

@patriksvensson
Copy link

You are right. I was assuming Arity was used for argument position.

@adamsitnik
Copy link
Member Author

You are right. I was assuming Arity was used for argument position.

It proves that the current name is not intuitive.

@reduckted
Copy link

Regarding Arity, in #1893 I suggested ValueCount.

@jonsequitur
Copy link
Contributor

This leads to the in my opinion akward situation regarding code readability/expressivity where for example Argument<int?> would behave differently (the argument being optional, thus not generating an error when omitted in the CLI invocation) than Argument<string?> (the argument being not optional, causing an error when omitted in the CLI invocation), despite the apparent semantics (and presumably the coder's intent) regarding optionality being the same for both Argument<int?> and Argument<string?>.

This is a really good point. We've discussed this a bit and it's likely we could make this more consistent. It might require a source generator, depending on whether the needed APIs support trimming.

I see the ambiguity here is being pretty similar to the confusion between Arity (the concept in the API, not the word) and the cardinality of the resulting parsed value. We can have an Arity of ExactlyOne and parse it to an array (having multiple cardinality), or we can have an Arity of OneOrMany and parse it to a single value (having single cardinality). By convention, and so most people don't have to think about it, we derive Arity from the type T of Option<T>/Argument<T>. But they're not required to match.

Ideally the name will make this separation of concepts as clear as possible.

@reduckted, I worry ValueCount might be too similar to ParseResult.GetValue and imply that it refers, for example, to the number of values in a parsed array.

@KalleOlaviNiemitalo
Copy link

Include Token in the name to distinguish it from the parsed values. So TokenCount, TokenCountLimit, TokenCountRange.

Is the enum type going to have a Cli prefix like in #1892?

@jonsequitur
Copy link
Contributor

Token is great! It reuses an existing term in the API. Count makes me think pedantic thoughts like, "Can there be a count before we've got the tokens?" The intent of Limit is clearer, but there are two of them, one lower and one upper. Range expresses that but loses the Limit concept. The returned type and its property names could do some work here, e.g. TokenCountLimits.Min and TokenCountLimits.Max? Do we even need a separate type for this? How about separate properties like Argument.MaxTokenCount and Argument.MinTokenCount?

Naming things before drinking enough coffee is hard.

Is the enum type going to have a Cli prefix like in #1892?

That might make sense. The guidance has been to avoid single-word type names so we focused on those. But I think that would be more consistent.

@tmds
Copy link
Member

tmds commented Mar 11, 2023

ExitCode
Similarly to CancellationToken I believe that each handler should just return an integer.

@adamsitnik do you mean: remove the InvocationContext.ExitCode in favor of int and Task<int> returns?

I think that's a good idea.

@tmds
Copy link
Member

tmds commented Mar 12, 2023

CommandLineConfiguration

I am looking at what type I need to use to invoke on, and I'm surprised this is the one.

I think CommandLine would be a more appropriate name.

CommandLineBuilder builder = new CommandLineBuilder(rootCommand);
CommandLine commandLine = builder.Build();
commandLine.Invoke(args);

This CommandLine type can have a Configuration property of type CommandLineConfiguration which holds the configuration for the command line.

@adamsitnik @jonsequitur thoughts?

@KalleOlaviNiemitalo
Copy link

CA1724: Type names should not match namespaces, assuming the namespace remains System.CommandLine.

@tmds
Copy link
Member

tmds commented Mar 13, 2023

CA1724: Type names should not match namespaces, assuming the namespace remains System.CommandLine.

Ah, that is the reason.

I think it would be good to change the names so the primary type to invoke on isn't named CommandLineConfiguration.

@KalleOlaviNiemitalo
Copy link

Users who don't need to configure anything in CommandLineConfiguration can already call CommandExtensions.Invoke(this Command, string, IConsole? = null), or similar for string[].

@KalleOlaviNiemitalo
Copy link

There's also #1905 for moving all methods from CommandExtensions into Command itself.

@tmds
Copy link
Member

tmds commented Mar 13, 2023

Users who don't need to configure anything in CommandLineConfiguration can already call CommandExtensions.Invoke(this Command, string, IConsole? = null), or similar for string[].

Even when the default configuration works for me and I can call Invoke on a Command, I'd still use the CommandLine type from my Main method.

This is subjective.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs discussion Further discussion required
Projects
None yet
Development

Successfully merging a pull request may close this issue.