Skip to content

Separate parse from invocation configurations #2606

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

jonsequitur
Copy link
Contributor

@jonsequitur jonsequitur commented Jul 1, 2025

This PR's goal is to separate the CommandLineConfiguration class into two separate classes, one of which configures parsing behaviors (now called ParserConfiguration) and the other of which configures invocation behaviors (InvocationConfiguration). InvocationConfiguration has four properties which were moved over from CommandLineConfiguration:

  • EnableDefaultExceptionHandler
  • ProcessTerminationTimeout
  • Output
  • Error

I've also removed the Parse, Invoke, and InvokeAsync methods from the former CommandLineConfiguration, since they're also available on ParseResult. The redundancy seemed unnecessary, although these invoke methods did include a Parse call, making the calling pattern more like the old, more succinct Command.Invoke methods.

Next steps:

  • In light of these changes, I think both of these classes need better names.
  • I think that passing a RootCommand to the CommandLineConfiguration constructor should not be necessary, and can lead to some unclear situations, such as:
var rootCommand = new RootCommand
{
    new Option<string>("-x")
};

var result = rootCommand.Parse(
    "-x which-root-command-controls-the-parser",
    new CommandLineConfiguration(
        new RootCommand
        {
            new Option<string>("-y")
        }));

@KalleOlaviNiemitalo
Copy link

I was going to comment that a suggest directive or a help action would need to be able to read the parsing configuration at invocation time, such as EnablePosixBundling. But I see ParseResult does have public properties for both configurations.

@jonsequitur jonsequitur changed the title Split invocation-time properties out of CommandLineConfiguration Separate parse from invocation configurations Jul 1, 2025
@jonsequitur jonsequitur marked this pull request as ready for review July 1, 2025 21:40
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it LGTM, but please see my comments with some suggestions for improvements. Thank you @jonsequitur !

public System.Threading.Tasks.Task<System.Int32> InvokeAsync(System.Threading.CancellationToken cancellationToken = null)
public System.Threading.Tasks.Task<System.Int32> InvokeAsync(InvocationConfiguration configuration, System.Threading.CancellationToken cancellationToken = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Parse methods, the configuration argument is optional (null by default, mapped into default config by the implementation). Perhaps Invoke should follow the same patter for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we change Parse to this overload approach versus coalescing the null parameter at runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If consistency is important here, another reason we might consider consolidating by splitting out Parse into two overloads (Parse() and Parse(ParserConfiguration)) is that that would not be a code-breaking change. Invoke has an InvokeAsync variant and this already has an optional CancellationToken argument, so adding an overload avoids breaking existing callers, since the InvocationConfiguration parameter comes before the CancellationToken parameter. (Of course there are a lot of breaking changes here so maybe this doesn't matter.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we change Parse to this overload approach versus coalescing the null parameter at runtime?

I strongly believe that we should not do that. IIRC this shape was approved in the API Design, it would be another breaking change and also another public API.

Last, but not least my goal with exposing Parse(CommandLineConfiguration? config = null) as the only overload was to make it easier for the end users to discover the fact that CommandLineConfiguration exists (in the past, it was quite hard to find out how to customize parsing: you would need to know about the existance of builder type etc).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoke has an InvokeAsync variant and this already has an optional CancellationToken argument, so adding an overload avoids breaking existing callers, since the InvocationConfiguration parameter comes before the CancellationToken parameter. (Of course there are a lot of breaking changes here so maybe this doesn't matter.)

That is very good point. However, from my observation it seems that users hardly ever provide CancellationToken to InvokeAsync as they usually call it from Main where they don't have any token.

public System.Boolean EnablePosixBundling { get; set; }
public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; }
public System.Void ThrowIfInvalid(Command command)
public class ParserConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost forgot about #1909. Should we simply remove this custom exception type and throw something defined by the BCL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought. I'd love suggestions on what would be an appropriate exception type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KalleOlaviNiemitalo has a very good breakdown here: #1909 (comment)

Copy link
Member

@adamsitnik adamsitnik Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvalidOperationException would be my best bet for now.

Or... even just removing ThrowIfInvalid? We don't use it anywhere outside of tests and it can be implemented with the public APIs we provide. It's also a bit confusing that it's part of the Config class, while what it does it checks the symbol hierarchy (and RootCommand is no longer part of the config)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion. This method has always been awkward.

We used to do this validation at runtime. Then we decided it was too performance-intensive so we said it should be used only in tests. My worry has been that people will assume the parser will catch these kinds of mistakes automatically, and it's not intuitive to implement some of the checks. (And I'm pretty sure we missed some cases.) Of course, ASP.NET left the analogous issue (routing validation) as an exercise for the user, so probably System.CommandLine should do the same.

@jonsequitur jonsequitur force-pushed the split-CommandLineConfiguration branch from 775d19a to 1f37d7d Compare July 2, 2025 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants