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).
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.