Getting Started with System.CommandLine (2023)

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


  • Why? – why do we want to use command line apps?
  • What? – what are .NET Core tools and System.CommandLine?
    • .NET Core Global Tool – what is a .NET Core global tool?
    • The Command Line App – what is a command line app?
  • The Basic-est – creating your first, single command app
    • Command Line Apps – how does a command line app work?
    • Our Greeting App – constructing the simple command
    • First Output – getting the first response from the app
    • Option Aliases – adding aliases to options
    • The Handler – handling an incoming command
  • The Sub-Command – adding a sub-command into the app
  • The Command Tree – adding multiple levels of commands
    • – make things a bit better
    • The Multiple Sub-Commands – adding multiple levels of commands

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

  • Basic – the very basic, single command app
  • Sub-Command – add single the sub-command
  • Command Tree – add multiple, nested commands

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


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.


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: CommandLineAppUsage: 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 restoredotnet builddotnet tool installdotnet 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 informationCommands: 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 echo times 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 informationCommands: 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 informationCommands: 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:

Top Articles
Latest Posts
Article information

Author: Ouida Strosin DO

Last Updated: 01/21/2023

Views: 5500

Rating: 4.6 / 5 (56 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Ouida Strosin DO

Birthday: 1995-04-27

Address: Suite 927 930 Kilback Radial, Candidaville, TN 87795

Phone: +8561498978366

Job: Legacy Manufacturing Specialist

Hobby: Singing, Mountain biking, Water sports, Water sports, Taxidermy, Polo, Pet

Introduction: My name is Ouida Strosin DO, I am a precious, combative, spotless, modern, spotless, beautiful, precious person who loves writing and wants to share my knowledge and understanding with you.