Search code examples
c#.net-corecommand-linesystem.commandline

Options not being added to a class which inherits from Command


I'm making a console app targeting dotnet core, using Microsofts System.CommandLine namespaces to parse commands. I know these namespaces are in beta and alpha stages, nothing is production here.

My package references, and versions

    <ProjectReference Include="..\lib\lib.csproj" />
    <PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
    <PackageReference Include="System.CommandLine.Rendering" Version="0.4.0-alpha.22272.1" />
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
    <PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />

Can anybody explain why it is the separated code doesn't work while the code nested in Main does? I'm assuming there's something happening (or rather not happening) when i try to implement this in the constructor that I can't see or don't understand but I can't for the life of me figure it out. Surely I'm not expected to assemble all the commands like that but between GPT, google and the official docs (very out of date) I'm stuck.

Criticism is welcome, I'm still learning.

My issue is when separating my Commands into classes, and adding all Options and Arguments from the constructor like so

public class RootCommand : Command
{
    public RootCommand()
        : base("cli", "WTNS root command.")
    {
        
        // all the options, args and initialization
        
        var executeOption = new Option<bool>(["--execute", "-e"],"If this option is set to true the request will be processed without starting the REPL.");
        executeOption.SetDefaultValue(true);
        AddOption(executeOption);
        
        Handler = CommandHandler.Create<bool>((option) => {
            if(!option)
            {
                Repl.Instance.Listen();
            } else
            {
                // Check
                Debug.Print($"###  success  ###");
                Console.ReadLine();
            }
        });
    }
}

and invoking the command from Main()

public sealed class Cli
{
    public static async Task Main(string[] args)
    {
        var rootCommand = new RootCommand();

        var parser = new CommandLineBuilder(rootCommand).UseDefaults().Build();
        parser.Invoke(args);

        Debug.Print("Command Name: " + rootCommand.Name);
        foreach (Option o in rootCommand.Options)
        {
            Debug.Print("Option: " + o.Name);
        }
    }
}

and running the project

dotnet run --execute true

I get the output

Unrecognized command or argument '--execute'.
Unrecognized command or argument 'true'.

Description:

Usage:
  cli [options]

Options:
  --version       Show version information
  -?, -h, --help  Show help and usage information

Command Name: cli
Option: version
Option: help

Where you can see it did not recognize the command as I expected (and by extension didn't invoke the Handler either).

However, this code, where the Option is added in Main() does work as expected and does invoke the Handler

public sealed class Cli
{
    public static async Task Main(string[] args)
    {
        var rootCommand = new RootCommand();

        var option = new Option<bool>(["--execute", "-e"],"If this option is set to true the request will be processed without starting the REPL.");
        rootCommand.AddOption(option);

        rootCommand.Handler = CommandHandler.Create<bool>(async (execute) =>
        {
            if(true)
            {
                Debug.WriteLine("option was true");
            }
        });

        var parser = new CommandLineBuilder(rootCommand).UseDefaults().Build();
        parser.Invoke(args);

        foreach (Option o in rootCommand.Options)
        {
            Debug.Print("Option :" + o.Name);
        }
    }
}

yielding the following output

option was true
Option : execute
Option : version
Option : help

EDIT: (Steve Wong)

Breakpoints

First Breakpoint Before rootCommand is initialized

Second Breakpoint As rootCommand is initialized. At this point, the constructor should have been called, and rootCommand.Handler should have been set, but it remains null as seen in the photo.

Third Breakpoint After rootCommand has been initialized.

Note that I haven't changed the code within the constructor (aside from a few changed comments) of RootCommand. The constructor still reads

public class RootCommand : Command
{
    public RootCommand()
        : base("cli", "WTNS root command.")
    {
        var executeOption = new Option<bool>(["--execute", "-e"],"If this option is set to true the request will be processed without starting the REPL.");
        executeOption.SetDefaultValue(true);

        AddOption(executeOption);
        
        Handler = CommandHandler.Create<bool>((option) => {
            if(!option)
            {
                // TODO: Send the command to the REPL
                Repl.Instance.Listen();
            } else
            {
                // TODO: Parse Command
                Console.WriteLine("#### Executing command ####");
                Console.ReadLine();
            }
        });
    }
}

EDIT 2:

I've provided a video where you can see I attempt to set breakpoints within the constructor of RootCommand It seems my breakpoints never get called no matter what. The code just runs as normal and closes when my only breakpoints are within RootCommand(){}. Despite the Option<bool> field containing a value, meaning the constructor must have ran.

I'm not a very experienced debugger but you can also see when i set a single breakpoint in CLI and the rest in RootCommand, all breakpoints in RootCommand go gray when i strart debugging and say No symbols loaded for this document Which seems odd I haven't stripped any symbols or compiled as release.

One more thing, the argument --execute is being passed from launch.json, that's why you don't see me typing it, it's there though.

Here's the clip of me setting some breakpoints

And if this helps, here's a cleaned copy of my entire project (OneDrive link).


Solution

  • My code was referencing the System.CommandLine.RootCommand class when calling new RootCommand() which I did not know existed, and therefore hadn't considered naming conflicts.

    Referring to my RootCommand class by its fully qualified name Wtns.Me.Lib.Commands.RootCommand fixed this problem.