Using Spectre.Console for CLI

24 Nov 2021

I have used the ManyConsole project for several years for processing command line arguments in console applications (in C# of course). I had been sitting in .NET Framework land doing corporate development writing web and console-based applications for automating the DevOps journey. After working through many homegrown implementations and variations of parsing string[] args, it was always lacking something, and never had a good help display, ManyConsole was a breath of fresh air. It was (almost) everything I was looking for.

Now that .NET 6 is out (The One SDK to Rule Them All), and developers are beginning to plan migrating from the older .NET Framework technologies, I began taking a fresh look at command line argument processing options. I looked for projects that met the following criteria:

Originally the ManyConsole author did not plan on supporting the .NET Standard 2.0, which was .NET Core at that time. I can see his reasoning, and at that time, I was still squarely within .NET Framework land, so my concern was negligible. You can look at the issue, logged in the repository, for the explanation and resulting implementation: Support for .NET standard

I went hunting around NuGet and considered the following packages. This list is not inclusive, as 500+ packages comes up for a search on commandline. I always include the venerable Mono.Options because, for me, that is where this all began.

Mono.Options

This package was the go to command line processing, in my programming library, for quite some time. Back when single executable programs were not really a thing, one could simply copy the source for Mono.Options into a project and be done. This is still a valid option for simple projects.

System.CommandLine

This was a Microsoft project before it wasn't. Now it appears to be managed by the .NET Foundation. IIRC, this package was supposed to be THE package for processing command line arguments. Hence its namespace: System.CommandLine.

This project seemed promising at first but over time became more annoying to use. Why? While I appreciate a good lambda to help me code I really prefer a simple programming style myself. IMO, the fancier you get, with lambdas strung together, and really long fluent style lines of code, the harder everything is to debug. I have never had any great affection for fluent style programming, or nested lambdas to get things done, because I like very predictable debugging. This library appeared to really fall in love with lambdas. I prefer a more modest approach. Additionally: the DragonFruit namespace. Enough said.

CommandLineUtils

This package has stood the test of time and powers a number of the tools that were written as part of my current professional projects. There is nothing wrong with this library, and it should work just fine for my current development needs.

It handles sub-commands, has an attribute based model, and can limit the amount of lambda code I write. Before you wonder about my strange obsession with avoiding lambdas, please wait, and let me explain. This block of code is pure nonsense, and not debuggable. It all executes at once, and you cannot predict what is happening, before it happens. To me, that is a key point for debugging. You need to make the decision about what is happening before the debugger executes a line of code. Only then can can step over the code and observe the result.

static async Task Main(string[] args) => await BuildCommandLine()
    .UseHost(_ => Host.CreateDefaultBuilder(),
        host =>
        {
            host.ConfigureServices(services =>
            {
                services.AddSingleton<IGreeter, Greeter>();
            });
        })
    .UseDefaults()
    .Build()
    .InvokeAsync(args);

However, I found Spectre.Console and it is, to me, all brand new and shiny. I cannot resist.

Spectre.Console

It has a programming model very similar to ManyConsole, so it seemed to fit for me. Yes, there are lambdas but, in this context, they seem to work. It may be that there is a single level present, but I always appreciate just calling the methods. The command structure replicates a lot of what I had been doing with ManyConsole. It could be just a level of comfort with the programming model, but in my view, that is what makes this library great. It works like you want it to, without learning something new (ahem, DragonFruit!?)

var app = new CommandApp();
app.Configure(config =>
{
    config.AddCommand<AddCommand>("add");
    config.AddCommand<CommitCommand>("commit");
    config.AddCommand<RebaseCommand>("rebase");
});

Spectre also provides way better screen handling. The progress bar, which looks a lot like Docker, is really nice. The status view is better than my traditional printing dots for showing continuous activity. There will be less clutter on the screen!

So far, the only downside that I can come up with is that examples are global and shown at the usage level. I would have preferred to have examples at the sub command level. Since I never included examples in my current applications this is something I can live with. Heck, it might even support this right now.

Frankly, there is not a lot to complain about. I have just begun working with this library, and I am enjoying it so far.

< back