command-line-api icon indicating copy to clipboard operation
command-line-api copied to clipboard

Implementing a default command

Open thinker227 opened this issue 2 years ago • 4 comments

I have an app using System.CommandLine which has two commands: compile and run, which are invoked using myapp compile path/to/some.file path/to/other.file and myapp run path/to/some.file respectively. However, I would like users to not necessarily have to specify compile, and instead just be able to do myapp path/to/some.file path/to/other.file to invoke compile implicitly. This is akin to how dotnet path/to/some.file is essentially an alias for dotnet exec path/to/some.file, and if you look at the help text for dotnet --help you'll see that it gives two separate usages. Is this possible to implement using System.CommandLine?

thinker227 avatar May 19 '23 18:05 thinker227

The .NET CLI has implemented a very customized version of help output. The top-level help just emits a raw string, but there are parts of the CLI (like dotnet new) that take help entirely into their own hands. I would suggest starting your search there if you'd like to see how we've done it.

baronfel avatar May 19 '23 18:05 baronfel

Makes sense I suppose, although how is the actual execution implemented such that you can specify either a file to run or one of several subcommands? Or is that also custom?

thinker227 avatar May 19 '23 19:05 thinker227

The CLI is a bit of a challenge to talk about here, since the behavior you see for dotnet path/to/dll is actually implemented in the 'dotnet host', a shim, native application that the Runtime team owns. None of that code is actually in what users think of as the .NET CLI. So let's look at dotnet new instead. It has two modes of execution:

  • invoking a template, or
  • invoking a known subcommand

The known subcommands are registered like normal commands here, but the NewCommand itself also has a handler function associated with it here. The System.CommandLine parser is smart enough to look at the positional tokens after dotnet new and decide if the token corresponds to one of the known subcommands - if it does, then the parser tries to parse and invoke the known subcommand, otherwise it invokes the 'Instantiate' command, which is responsible for actually invoking the template you specified.

Hope that helps!

baronfel avatar May 19 '23 19:05 baronfel

For future readers: I've hacked this together which kind of works. If you also override the default help text then you can make it quite nice.

/// <summary>
/// Adds middleware which invokes a default command if the root command is invoked.
/// </summary>
/// <param name="builder">The source <see cref="CommandLineBuilder"/>.</param>
/// <param name="rawArgs">The raw command-line arguments.</param>
/// <param name="defaultCommand">The command to use as the default command.</param>
public static CommandLineBuilder UseDefaultCommand(
    this CommandLineBuilder builder,
    string[] rawArgs,
    Command defaultCommand) =>
    builder.AddMiddleware(ctx =>
    {
        // If this is not the root command that is invoked then don't do anything.
        if (ctx.ParseResult.CommandResult.Command != ctx.ParseResult.RootCommandResult.Command) return;

        // This should probably have the same configuration used by the rest of your CLI.
        // However make sure to not have this middleware recursively invoke itself.
        var builder = GetDefaultBuilder(defaultCommand);
        
        var parser = builder.Build();
        var result = parser.Parse(rawArgs);

        // Override the parse result with the new one.
        ctx.ParseResult = result;
    });
builder.UseDefaultCommand(rawArgs, yourDefaultCommand);

thinker227 avatar May 30 '23 23:05 thinker227