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

Invoke a parent command without specifying its subcommands

Open qui8t opened this issue 2 years ago • 9 comments

In my use case, both the following invocations are expected.

$ myprog --load checkpoint.json
$ myprog process --file test.json

where the root command myprog with the subcommand process can be called without specifying a subcommand. Is this use-case currently supported in System.CommandLine?

Using a code similar to the following, I get Required command was not provided error when making the first invocation above.

using System.CommandLine;

var loadOpt = new Option<FileInfo?>("--load");
var rootCmd = new RootCommand() { loadOpt };

var fileOpt = new Option<FileInfo?>("--file");
var subCmd = new Command(name: "process") { fileOpt };

rootCmd.AddCommand(subCmd);

qui8t avatar Aug 08 '22 11:08 qui8t

The Required command was not provided error is generated by ParseResultVisitor.ValidateCommandHandler. It looks like you could just give the parent command a non-null handler and then it would be OK.

https://github.com/dotnet/command-line-api/blob/4b605851d45131db2374ba711f2443040521d702/src/System.CommandLine/Parsing/ParseResultVisitor.cs#L417-L434

KalleOlaviNiemitalo avatar Aug 08 '22 11:08 KalleOlaviNiemitalo

Thanks! To confirm, adding the following fixes the issue (though I guess it would have been better if it was not required :)).

rootCmd.SetHandler(x => { });

qui8t avatar Aug 08 '22 13:08 qui8t

I wonder if it would be possible to specify options that can be used without subcommands.

For instance, with the following:

var workingDirOption = new Option<DirectoryInfo?>("--working-dir");
rootCmd.AddGlobalOption(workingDirOption);

Allowed:

$myprog --load checkpoint.json

Disallowed:

myprog --working-dir .

qui8t avatar Aug 08 '22 13:08 qui8t

I don't understand what you request. The following program built with System.CommandLine 2.0.0-beta4.22272.1

using System.CommandLine;

var loadOpt = new Option<FileInfo?>("--load");
var rootCmd = new RootCommand() { loadOpt };

var fileOpt = new Option<FileInfo?>("--file");
var subCmd = new Command(name: "process") { fileOpt };

rootCmd.AddCommand(subCmd);

var workingDirOption = new Option<DirectoryInfo?>("--working-dir");
rootCmd.AddGlobalOption(workingDirOption);

rootCmd.SetHandler(x => { });

return rootCmd.Invoke(args);

gives the following results when executed with different command lines:

  • --load checkpoint.json: OK
  • process --load checkpoint.json: Unrecognized command or argument
  • --working-dir .: OK
  • process --working-dir .: OK
  • --file stuff: Unrecognized command or argument
  • process --file stuff: OK
  • --not-defined at-all: Unrecognized command or argument
  • process --not-defined at-all: Unrecognized command or argument

This seems to cover all the combinations already. Or is your request about other subcommands?

KalleOlaviNiemitalo avatar Aug 08 '22 14:08 KalleOlaviNiemitalo

I want the following to fail.

--working-dir .: OK

Without rootCmd.SetHandler(x => { });. In other words, I am curious about how to change the code for the following use case.

  • --load checkpoint.json: OK
  • --working-dir .: FAIL

qui8t avatar Aug 08 '22 18:08 qui8t

So you want --working-dir to behave the same way as --file?

KalleOlaviNiemitalo avatar Aug 08 '22 19:08 KalleOlaviNiemitalo

Yes, though I am adding the workingDirOpt to the rootCmd since I want every subcommand to have that option. So, I am trying to avoid the following.

var subCmd1 = new Command(name: "process") { workingDirOpt, fileOpt };
var subCmd2 = new Command(name: "delete")  { workingDirOpt };
var subCmd3 = new Command(name: "post")    { workingDirOpt, anotherOpt, anotherOpt2 };

So, I am interested in the following invocations:

  • OK:

    $ myProg --load checkpoint.json
    $ myProg process --working-dir . --file x.txt
    $ myProg delete --working-dir .
    
  • FAIL:

    $ myProg
    $ myProg --working-dir .
    $ myProg process --load checkpoint.json
    $ myProg process --load checkpoint.json --working-dir . --file x.txt
    

qui8t avatar Aug 08 '22 21:08 qui8t

How about deriving a class from Command and adding the option in the constructor?

A validator on the root command (or on the option?) could also be a way to reject the option if no subcommand is provided, but then the option would still appear in help and completions… those may be customizable but it would get yet more complex.

KalleOlaviNiemitalo avatar Aug 08 '22 22:08 KalleOlaviNiemitalo

That seems doable, though a bit overkill IMHO, and comes with issues in the help display as you suggested.

qui8t avatar Aug 09 '22 12:08 qui8t