Search code examples
c#.net-corecommand-line-interfacecommand-line-argumentssystem.commandline

How to pass multiple values to the same System.CommandLine option?


I would like to pass several numbers to a C# console app and have prepared a .Net Fiddle to demonstrate my issue:

private const long DEFAULT_ALPHA_VALUE = 1234567890L;
private static long[] alphas = { DEFAULT_ALPHA_VALUE };

public static async Task Main()
{
    Option<long[]> alphaOption = new
    (
        aliases: new[] { "-a", "--alpha" },
        getDefaultValue: () => new[] { DEFAULT_ALPHA_VALUE },
        description: "Numerical alpha values"
    );

    RootCommand rootCommand = new("A test app for multiple numerical values option");
    rootCommand.AddGlobalOption(alphaOption);
    rootCommand.SetHandler(a => { alphas = a; }, alphaOption);
    
    await RunInvokeAsync(rootCommand, "-a", "1234");
    await RunInvokeAsync(rootCommand, "-a", "1234", "5678"); // Unrecognized command or argument '5678'.
    await RunInvokeAsync(rootCommand, "-a", "1234 5678"); // Cannot parse argument '1234 5678' for option '-a' as expected type 'System.Int64'.
    await RunInvokeAsync(rootCommand, "-a", "1234,5678"); // Cannot parse argument '1234 5678' for option '-a' as expected type 'System.Int64'.
}

private static async Task RunInvokeAsync(RootCommand rootCommand, params string[] args)
{
    int status = await rootCommand.InvokeAsync(args);
    Console.WriteLine($"args: {JsonSerializer.Serialize(args)}, status: {status}, alphas: {JsonSerializer.Serialize(alphas)}");
}

I was expecting being able to pass several numbers by running one of the following commands at the CLI:

dotnet run --project MultipleValuesOption.csproj -- -a 1234 5678
dotnet run --project MultipleValuesOption.csproj -- -a 1234,5678

But as you can see in the screenshot there are runtime errors:

screenshot showing errors

My question is: how do you pass several numbers, am I picking them wrongly in my C# code or do I maybe call the C# app in a wrong way?

Update:

I have followed the great suggestion by Serg and have added the { AllowMultipleArgumentsPerToken = true }, but unfortunately there is still a problem, when you add a second option as shown in my new .Net Fiddle:

private const long DEFAULT_ALPHA_VALUE = 13579L;
private static long[] alphas = { DEFAULT_ALPHA_VALUE };
private const long DEFAULT_BETA_VALUE = 24680L;
private static long[] betas = { DEFAULT_BETA_VALUE };

public static async Task Main(string[] args)
{
    Option<long[]> alphaOption = new
    (
        aliases: new[] { "-a", "--alpha" },
        getDefaultValue: () => new[] { DEFAULT_ALPHA_VALUE },
        description: "Numerical alpha values"
    )
    { 
        AllowMultipleArgumentsPerToken = true,
    };

    Option<long[]> betaOption = new
    (
        aliases: new[] { "-b", "--beta" },
        getDefaultValue: () => new[] { DEFAULT_BETA_VALUE },
        description: "Numerical beta values"
    )
    {
        AllowMultipleArgumentsPerToken = true,
    };

    RootCommand rootCommand = new("A test app for multiple numerical values option");
    rootCommand.AddGlobalOption(alphaOption);
    rootCommand.AddGlobalOption(betaOption);
    rootCommand.SetHandler(a => { alphas = a; }, alphaOption);
    rootCommand.SetHandler(b => { betas = b; }, betaOption);

    await RunInvokeAsync(rootCommand, args);
    await RunInvokeAsync(rootCommand, "-a", "1234", "5678");
}

private static async Task RunInvokeAsync(RootCommand rootCommand, params string[] args)
{
    int status = await rootCommand.InvokeAsync(args);
    Console.WriteLine($"args: {JsonSerializer.Serialize(args)}, status: {status}, alphas: {JsonSerializer.Serialize(alphas)}, betas: {JsonSerializer.Serialize(betas)}");
}

The output shows that the alphas values are not really picked up from the command line and thus it stays at the default value of 13579:

args: [], status: 0, alphas: [13579], betas: [24680]
args: ["-a","1234","5678"], status: 0, alphas: [13579], betas: [24680]

Solution

  • Try this

        Option<long[]> alphaOption = new
        (
            aliases: new[] { "-a", "--alpha" },
            getDefaultValue: () => new[] { DEFAULT_ALPHA_VALUE },
            description: "Numerical alpha values"
        )
        { 
            AllowMultipleArgumentsPerToken = true,
        };
    
        Option<long[]> betaOption = new
        (
            aliases: new[] { "-b", "--beta" },
            getDefaultValue: () => new[] { DEFAULT_BETA_VALUE },
            description: "Numerical beta values"
        )
        {
            AllowMultipleArgumentsPerToken = true,
        };
    

    The point of interest is a AllowMultipleArgumentsPerToken = true lines.

    And then register these options as follows

        RootCommand rootCommand = new("A test app for multiple numerical values option");
        rootCommand.AddGlobalOption(alphaOption);
        rootCommand.AddGlobalOption(betaOption);
        rootCommand.SetHandler((a,b) => 
        {
            alphas = a;
            betas = b;
        }, alphaOption, betaOption);
    

    In this case the following values will be handled correctly

        await RunInvokeAsync(rootCommand, "-a", "1234");
        await RunInvokeAsync(rootCommand, "-a", "1234", "5678");
        await RunInvokeAsync(rootCommand, "-a", "1234", "5678", "-b", "1111", "2222");
    

    The latter two will still fail, but it's expected as you are trying to use "1234 5678" or "1234,5678" as a single argument, which is obviously wrong and can't be parsed as number. To emulate such a cases from the command line, the user should use additional escaping the explicitly tell the interpreter to treat input as a single parameter, so it should not be a problem for you as developer.