Getting Started with System.CommandLine

Before I get distracted with a story, this post is going to be a quick intro into creating your first command line app, using the specifically named System.CommandLine library.

All the code for this post lives in https://github.com/mattleibow/CommandLineApp

Contents

If you are just here for code snippets, you can jump to it:

But before we get too crazy, let’s talk about the why of the concept…

Why?

I enjoy writing code… Just in case you don’t know by now. A bit too much, I might add. But, I also enjoy setting up unit tests and CI/CD to prove that my code works.

Azure DevOps and GitHub Actions are powerful and allow me to pretty much do whatever I want. I also combine this with Cake to get that sweet, sweet C# scripting ability. I could write everything in the pipeline YAML, but this has 2 downsides:

  1. No local execution for testing
  2. No sharing between pipelines or tooling (lock in)

One way to avoid all this is to use a big Cake script so that I can just have a single line of execution in my build.cake:

dotnet cake

This is pretty cool, but it now makes me write more C# code (which is awesome) when all I wanted to do was a generic action. I could create a Cake addin, but this forces me to use Cake – even when I don’t need to. So the downsides of this are:

  1. Slightly more complex task of creating a Cake addin
  2. No sharing with anything other than Cake scripts

So, what can we do to avoid all the downsides, but still have a “tool” that allows us to generically run a chunk of work, but have no pipeline or scripting restrictions?

Introducing the (not so new) .NET Core Global Tools! This is pretty much a way to write a small console app that can be quickly and easily installed from NuGet. All you need is an install of .NET Core – which is most likely already available to you… since you are reading about .NET and C# development.

For example, to install the Cake tools for running your build script, you just need:

dotnet tool install -g cake.tool

That is it! It will pull down and install the tool, and then you can call:

dotnet cake

All right, so now that we know we want to use .NET Core tools, how do we get started? But, before we do that, we need to have a look at what a .NET Core tool is and what System.CommandLine does.

What?

A .NET Core tool is pretty much an ordinary NuGet package, but has the ability to be installed as a command line tool – rather than just as a dependency in another project.

.NET Core Global Tool

You can read more on the docs website, but here is an example .csproj that can be packed into a tool:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <PackAsTool>true</PackAsTool>
    </PropertyGroup>

</Project>

The special line (or rather property) is the PackAsTool property. This can also be set on the command line during a build, but since this is only going to be a tool, I add it to my project file.

That property only affects the build when running a pack, so everything else is still the same. When wanting to test out the app during development, you can still use the old:

dotnet run -- <args>

The -- indicates that you are going to pass whatever is to the right directly to the app itself.

The Command Line App

But now, let us move into the code… First, when creating a console app, we are greeted with some exciting code:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Even though this is pretty exciting, it is not really useful. If we want to make an actual app, we have to manually parse the args parameter and then do work. We also have to let the caller know if things went well or not using the exit code. We might even want to do asynchronous code using async/await.

So, I like the more modern, more powerful version:

class Program
{
    static void async Task<int> Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        return 0;
    }
}

Look at that sweet command line app that is capable of anything!

But it does nothing yet! We still have to take that unassuming string[] args and turn it into some real work.

We could do that ourselves… we could, but we are not crazy… at least not in that way. So, this is where we pull in the fancy new System.CommandLine package. You can read all the docs and wikis and samples in the command-line-api repository, but I am just going to jump in with some example setups.

The Basic-est

As with all things, I want to jump in at the deep end – I actually did this before this very post – and flail around for a few hours/days (is there a difference?) and then eventually have a working app. But now that I look smart, I’ll start at the beginning.

Command Line Apps

To get started, many apps don’t need asynchronous code and they only have a success or a crashed result. The crash automatically sets the exit code to 1 (bad), and a “no crash” to 0 (good). So, for this example, we are going to stick with the synchronous void return:

class Program
{
    static void Main(string[] args)
    {
    }
}

Now, in order to add the command line parsing, we can install the System.CommandLine package and then when that is done, start writing code. All apps start with a RootCommand and may or may not have sub commands. Most commands have a set of Option items and possibly an Argument item. And then, there is a Handler that actually does the work.

So, let’s have a look at a basic command:

tool --option-a --option-b value "argument value"

This has a few parts:

  • tool is the command/app name
  • --option-a is a flag option – it signifies a simple boolean value
  • --option-b value is a value option – it allows a value to be associated with it
  • "argument value" is the argument that is passed – it typically provides the information that is core to a command

If we are to translate this into a method signature, we might do this:

ExecuteTool(bool optionA, string? optionB, string argument);

And this is exactly what System.Commandline does for us!

Our Greeting App

So, let’s finally get to the part where we write out the bits to do this! Enough delays!

The first thing we do is create the objects and set a handler. We are going to use a RootCommand with one Argument item and two Option items. We are then setting the Handler using the helper method CommandHandler.Create<T>. Finally, we call Invoke to actually run the app.

static int Main(string[] args)
{
    var cmd = new RootCommand
    {
        new Argument<string>("name", "Your name."),
        new Option<string?>("--greeting", "The greeting to use."),
        new Option("--verbose", "Show the deets."),
    };

    cmd.Handler = CommandHandler.Create<string, string?, bool, IConsole>(HandleGreeting);

    return cmd.Invoke(args);
}
static void HandleGreeting(string name, string? greeting, bool verbose, IConsole console)
{
    // TODO: a great app
}

The IConsole parameter in the handler is a magical one. It allows us to write to the console without taking a strict dependency on the Console type. We don’t need it, but it does allow us to keep our hands clean.

If we look at the definition of the command, we can see that there are some items that map to the signature of the handler method. This is one of the very exciting things about System.CommandLine, it will map from the strings in the args to the strongly typed arguments in the handler.

It can do the primitives as well as enum values. It also has a way to control how many of the options are allowed and what values can be passed to them. There are even ways to hook up validation so that by the time the handler is called, everything is ready to go and the values are clean.

First Output

Let’s run the app! (I am using dotnet run -- so I don’t have to pack and install)

Required argument missing for command: CommandLineApp

Usage:
  CommandLineApp [options] <name>

Arguments:
  <name>    Your name.

Options:
  --greeting <greeting>    The greeting to use.
  --verbose                Show the deets.
  --version                Show version information
  -?, -h, --help           Show help and usage information

Just look at that nice error message and help output! We did nothing, and we are already getting things working! We get a nice --help and --version args as well! It’s amazing!

But hey! What is that? We only have the long form of the options!

Option Aliases

Imagine how tired we will get typing in “–greeting”. So many characters! We want to do “-g”! How do we do that? Well the first value in the Option constructor allows us to pass an array of aliases:

new Option<string?>(new[] { "--greeting", "-g" }, "The greeting to use."),
new Option(new[] { "--verbose", "-v" }, "Show the deets."),

OK, I got distracted already… If we run dotnet run -- --help then we get a nice output:

Usage:
  CommandLineApp [options] <name>

Arguments:
  <name>    Your name.

Options:
  -g, --greeting <greeting>    The greeting to use.
  -v, --verbose                Show the deets.
  --version                    Show version information
  -?, -h, --help               Show help and usage information

Much better! Now that I fixed that, we can continue with the implementation.

The Handler

Our HandleGreeting method is a normal C# method:

static void HandleGreeting(string name, string? greeting, bool verbose, IConsole console)
{
    if (verbose)
        console.Out.WriteLine($"About to say hi to '{name}'...");

    greeting ??= "Hi";
    console.Out.WriteLine($"{greeting} {name}!");

    if (verbose)
        console.Out.WriteLine($"All done!");
}

This is very simple, we have the 3 values we requested from the command line (with the optional greeting) and the magical IConsole from the library. We then go ahead and run as we would in any normal console app.

If I run this with dotnet run -- Matthew, and this is the output:

Hi Matthew!

We can also run this now with those fancy options. For example, to say “good morning” to a girl named “Indry” using the verbose output, we can run:

dotnet run -- -g "Selamat pagi" Indry --verbose

As you can see, we are mixing the short form and the long form of options as well as the placement of the arguments. The output of this is:

About to say hi to 'Indry'...
Selamat pagi Indry!
All done!

Pretty neat! We did no actual work. We just described the options we want, and then made a handler to match. System.CommandLine did all the rest.

The Sub-Command

Right… Now, what happens if we want to create a complex app with loads of features? For example, the .NET app itself? When you run dotnet, you have a good selection of “commands” or “sub apps” or “sub commands”. For example, there are commands like “restore”, “build” and “tool”.

Commands are a way of breaking up a single tool into multiple sub-tools to avoid the need for many tools. But it also allows for grouping of commands as a hierarchy.

For example, dotnet has several commands and sub commands:

dotnet
  restore
  build
  tool
    install
    uninstall

With these options, we can do things like this:

dotnet restore
dotnet build
dotnet tool install
dotnet tool uninstall

Instead of having many tools, we got one. In steam of having a complex set of arguments, we have commands and sub-commands.

And, we can do this as well by simply nesting our commands when we set it up. Before we go extreme, we are going to take our simple app, and make it into a sub-command. This is not too useful as is, but it demonstrates the bits needed:

static int Main(string[] args)
{
    var greeting = new Command("greeting", "Say hi.")
    {
        new Argument<string>("name", "Your name."),
        new Option<string?>(new[] { "--greeting", "-g" }, "The greeting to use."),
        new Option(new[] { "--verbose", "-v" }, "Show the deets."),
    };

    greeting.Handler = CommandHandler.Create<string, string?, bool, IConsole>(HandleGreeting);

    var cmd = new RootCommand
    {
        greeting
    };

    return cmd.Invoke(args);
}

That is it! No changes needed to the handler at all. The handler works exactly the same whether it is the root command or a sub-command. But this opens up the possibility to do much more. If we run the app now with dotnet run -- --help, we get a slightly different output:

Usage:
  CommandLineApp [options] [command]

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

Commands:
  greeting <name>    Say hi.

We can also get the help for the greeting command using dotnet run -- greeting --help:

greeting:
  Say hi.

Usage:
  CommandLineApp greeting [options] <name>

Arguments:
  <name>    Your name.

Options:
  -g, --greeting <greeting>    The greeting to use.
  -v, --verbose                Show the deets.
  -?, -h, --help               Show help and usage information

The Command Tree

OK, so we have it done. We know how to create an app. We know how to add a sub-command. So, now we need to add moar!

Helper Extensions & Reflection Handlers

But, before we get too excited, I want to add a little tweak. If you look at the construction of the commands, you will notice the slightly different way in which I set the Handler property. This is correct, but it has a few drawbacks:

  1. It reaches a limit of 7 parameters in the generic arguments
  2. It is annoying to line up the generic arguments with the method signature
  3. It breaks the cool structure of the initializers

What to do? Extension Methods! Reflection!

Before you get all nervous, the reflection idea is the alternate way to connect the handlers with commands, so we are not going too crazy. And the extension method is really neat. And simple:

static Command WithHandler(this Command command, string name)
{
    var flags = BindingFlags.NonPublic | BindingFlags.Static;
    var method = typeof(Program).GetMethod(name, flags);

    var handler = CommandHandler.Create(method!);
    command.Handler = handler;
    return command;
}

Would you look at that? Nice and simple. It is basically using reflection to find the MethodInfo and pass that to CommandHandler.Create. It then assigns that handler to the command.

If we take our basic sub-command, we can simplify the code a bit:

static int Main(string[] args)
{
    var cmd = new RootCommand
    {
        new Command("greeting", "Say hi.")
        {
            new Argument<string>("name", "Your name."),
            new Option<string?>(new[] { "--greeting", "-g" }, "The greeting to use."),
            new Option(new[] { "--verbose", "-v" }, "Show the deets."),
        }.WithHandler(nameof(HandleGreeting))
    };

    return cmd.Invoke(args);
}

The handler is now inline and does not break the hierarchy layout. We could even make this an expression bodied member and have the Invoke called on the closing brace of the RootCommand, but I leave that up to you.

Anyways, lets get back!

The Multiple Sub-Commands

We can take all that we have done so far and join it all and keep adding new commands and sub-commands. And that is what I have done!

I am creating a 3 command app that supports some exciting invocations:

app greeting ...
app echo times ...
app echo forever ...

Check it out:

public static async Task<int> Main(string[] args)
{
    var cmd = new RootCommand
    {
        new Command("greeting", "Say hi.")
        {
            new Argument<string>("name", "Your name."),
            new Option<string?>(new[] { "--greeting", "-g" }, "The greeting to use."),
            new Option(new[] { "--verbose", "-v" }, "Show the deets."),
        }.WithHandler(nameof(HandleGreeting)),
        new Command("echo", "Stop copying me!")
        {
            new Command("times", "Repeat a number of times.")
            {
                new Argument<string>("words", "The thing you are saying."),
                new Option<int>(new[] { "--count", "-c" }, description: "The number of times to copy you.",
                    getDefaultValue: () => 1),
                new Option<int>(new[] { "--delay", "-d" }, description: "The delay between each echo.",
                    getDefaultValue: () => 100),
                new Option(new[] { "--verbose", "-v" }, "Show the deets."),
            }.WithHandler(nameof(HandleEchoTimesAsync)),
            new Command("forever", "Just keep repeating.")
            {
                new Argument<string>("words", "The thing you are saying."),
                new Option<int>(new[] { "--delay", "-d" }, description: "The delay between each echo.",
                    getDefaultValue: () => 100),
                new Option(new[] { "--verbose", "-v" }, "Show the deets."),
            }.WithHandler(nameof(HandleEchoForeverAsync)),
        },
    };

    return await cmd.InvokeAsync(args);
}

I am doing everything there! Asynchronous Task<int>, CancellationToken, void, default values and much much more!

My handlers are not to crazy either! It is amazing what this can do:

static async Task<int> HandleEchoTimesAsync(
    string words,
    int count,
    int delay,
    bool verbose,
    IConsole console,
    CancellationToken cancellationToken);

static async Task<int> HandleEchoForeverAsync(
    string words,
    int delay,
    bool verbose,
    IConsole console,
    CancellationToken cancellationToken);

private static void HandleGreeting(
    string name,
    string? greeting,
    bool verbose,
    IConsole console);

You don’t have to put everything into a single, massive class, but you can split it up into namespaces, multiple classes, nested classes or anything you like. System.CommandLine is the bit between string[] args and your methods.

If we run the app with dotnet run -- --help, we have a nice output:

Usage:
  CommandLineApp [options] [command]

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

Commands:
  greeting <name>    Say hi.
  echo               Stop copying me!

We can also run help for a command! Try dotnet run -- echo --help:

More commands! Look:

echo:
  Stop copying me!

Usage:
  CommandLineApp echo [options] [command]

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

Commands:
  times <words>      Repeat a number of times.
  forever <words>    Just keep repeating.

I think my job here is done!

Start today. Make your command line apps cool. Be cool.

You can check out all this code on my GitHub: https://github.com/mattleibow/CommandLineApp

2 thoughts on “Getting Started with System.CommandLine

  1. This is great!!

    Tip for folks trying this out: The most common issue we see is people having different names for their options/arguments and their method parameters. Kebab-casing and capitalization is handled just fine. But System.CommandLine can’t match “file” with “FileName” (the last example I saw).

    Also, there is a small type:

    “Imagine how tired we will get typing in “–greeting”. So many characters! We want to do “-g”! How do we do that? Well the first value in the Option constructor allows us to pass an array of options:”

    I believe the last word should be “aliases”.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s