Using System.CommandLIne to parse options and commands with options

3k views Asked by At

Scenario: I want to parse a console app's command line that has a number of options (that don't relate to a command), and a command that has a number of options. I've simplified what I'm trying to into a fictitious example. E.g. myapp --i infile.dat --o outfile.dat conversion --threshold 42

Problem: I'm finding that if I put the "conversion" command and it's option on the command-line then only its handler gets called, but not the handler for the root command. So I have no way to determine the values of the root command's --i and --o options.

(Conversely, if I omit the "conversion" command then root command's handler only is called - which is what i would expect.)

Example code:

public class Program
{
    public static async Task<int> Main(string[] args)
    {
        // Conversion command
        var thresholdOpt = new Option<int>("--threshold");
        var conversionCommand = new Command("conversion") { thresholdOpt };
        conversionCommand.SetHandler(
            (int threshold) => { Console.WriteLine($"threshold={threshold}"); },
            thresholdOpt);

        // Root command
        var infileOpt = new Option<string>("--i");
        var outfileOpt = new Option<string>("--o");
        var rootCommand = new RootCommand("test") { infileOpt, outfileOpt, conversionCommand };
        rootCommand.SetHandler(
            (string i, string o) => { Console.WriteLine($"i={i}, o={o}"); },
            infileOpt, outfileOpt);

        return await rootCommand.InvokeAsync(args);
    }
}

Unexpected outputs:

> myapp --i infile.dat --o outfile.dat conversion --threshold 42

threshold=42

In the above I expect to see the value for the --i and --o options, as well as the threshold options associated with the conversion command, but the root command's handler isn't invoked.

Expected outputs:

> myapp --i infile.dat --o outfile.dat 

i=infile.dat, o=outfile.dat

> myapp conversion --threshold 42

threshold=42

The above are what I'd expect to see.

Dependencies: I'm using System.CommandLine 2.0.0-beta3.22114.1, System.CommandLine.NamingConventionBinder v2.0.0-beta3.22114.1, .net 6.0, and Visual Studio 17.1.3.

I'd be grateful for help in understanding what I'm doing wrong. Thanks.

1

There are 1 answers

3
Guru Stron On

Based on the docs sample it seems only one verb gets executed. For example next:

var rootCommand = new RootCommand();
rootCommand.SetHandler(() => Console.WriteLine("root"));
var verbCommand = new Command("verb");
verbCommand.SetHandler(() => Console.WriteLine("verb"));
rootCommand.Add(verbCommand);
var childVerbCommand = new Command("childverb");
childVerbCommand.SetHandler(() => Console.WriteLine("childverb"));
verbCommand.Add(childVerbCommand);

return await rootCommand.InvokeAsync(args);

For no arguments will print root, for verb will print verb and for verb childverb will print childverb.

So if you need multiple actions performed it seems you will need to use another approach (for example manually processing rootCommand.Parse() result).

If you just want "--i" and "--o" accessible for conversion then add them to corresponding command:

// actually works without specifying infileOpt, outfileOpt on conversionCommand 
// but should be still present on the root one
// also rootCommand.AddGlobalOption can be a more valid approach
var conversionCommand = new Command("conversion") { thresholdOpt, infileOpt, outfileOpt}; 

// add here for handler
conversionCommand.SetHandler(
    (int threshold, string i, string o) => { Console.WriteLine($"threshold={threshold}i={i}, o={o}"); },
    thresholdOpt, infileOpt, outfileOpt);