system.commandline on laptop

Demystifying System.CommandLine

Overviewing the System.CommandLine Library

“All apps are command-line apps” – Jon Sequeria, Principle Software Engineer on System.CommandLine.

It took me a while to wrap my head around Jon’s statement, but the longer I have thought about it, the more I agree. It is possible to invoke apps from the command line, but many apps do not leverage this interface.

I have had the privilege of working on the System.CommandLine library for the last several years. With the Beta 3 release and the official documentation now added, the library is getting much closer to the full General Availability (GA) release. It is an amazing library with many features, including tab completion and trim compatibility, while still being very performant.

It is worth mentioning that the public API of the project has been under constant development and improvement. As of writing this article, the latest version posted on NuGet.org is 2.0.0-beta3.22114.1 (posted on Feb 18th, 2022). You need to install pre-release NuGet packages because the library is still in beta.

System.CommandLine Terminology

System.CommandLine strives to ensure the parsing of the command line is consistent and provides both a good developer experience and a good end-user experience. Providing a universal experience can be quite difficult as many users are familiar with a mix of Bash, batch scripting, PowerShell, and others. Underlying many command-line interfaces is the Portable Operating System Interface (POSIX). Among other things, POSIX defines the syntax for command-line applications. Though there is quite a lot of information in the specification, three key terms help clarify the System.CommandLine API.

Key Terms to Understand the System.CommandLine API

Command: A command is an action or verb that the command-line app performs. The very top-level command, the name of your executable (also referred to as arg0), is the root command. A command may also contain subcommands. When running a CLI application, only a single command executes. Using the dotnet CLI as an example:

> dotnet publish

In this example, dotnet is the root command, and publish is the executed subcommand.

Option: An optional named parameter for a command.

> dotnet publish --no-build

In this example, --no-build is an option for the publish command.

Argument: A value passed to either a command or an option. Arguments may have an arity, indicating how many values they can hold.

> dotnet publish --configuration Release

In this example, --configuration is an option to the publish command that accepts Release as an argument. It only accepts a single value; therefore, it has an arity of one.

In addition, the command-line interface supports more advanced concepts, including option argument delimiters, bundling, and directives.

Getting Started with System.CommandLine

In this example, we build a simple console application called LiveFile. It runs a countdown timer and outputs the remaining time to a file (this can be useful for running a countdown timer in OBS).

First, we will configure the command-line syntax for our simple app using C# top-level statements.

Option<DateTime> countdownToOption = new(
    name: "--countdown-to",
    description: "The target time to count down to");
Option<FileInfo> outputFileOption = new(
    name: "--output-file", 
    getDefaultValue: () => new FileInfo("output.txt"), 
    description: "The output file to write to");
Option<Verbosity> verbosityOption = new(
    aliases: new[] { "--verbosity", "-v" },
    description: "The verbosity of the output");

RootCommand rootCommand = new(description: "A countdown timer application")
{
    countdownToOption,
    outputFileOption,
    verbosityOption,
};
rootCommand.SetHandler(
    async (DateTime countdownTo, FileInfo outputFile, Verbosity verbosity, IConsole console) =>
    {
        //... implementation removed for brevity
    },
    countdownToOption,
    outputFileOption,
    verbosityOption);
await rootCommand.InvokeAsync(args);

The full code for this example is available on GitHub.

The initial setup is quite simple. We declare the options for our root command. These options all contain a type (the generic parameter specified on the Option class), which defines the argument type for that option. Finally, each of the options is added to the rootCommand. This command is simply the top-level command that is the name of the application, which in this case with be “LiveFile.”

A visual representation of this configuration is visible in the application’s help output. The help output generated for you by System.CommandLine is customized to suit your needs.

> .\LiveFile --help
Description:
  A countdown timer application

Usage:
  LiveFile [options]

Options:
  --countdown-to <countdown-to>            The target time to count down to
  --output-file <output-file>              The output file to write to [default: output.txt]
  -v, --verbosity <Detailed|Normal|Quiet>  The verbosity of the output
  --version                                Show version information
  -?, -h, --help                           Show help and usage information

The names, aliases, and descriptions specified on the options and root command display in the application’s help output. In addition to the names and descriptions, the application’s help output also displays the default value for the outputFile option and the enum members for our custom Verbosity enum (which contains three members: Detailed, Normal, and Quiet).

Finally, it attaches a handler to the root command. When running the application, only a single command (or subcommand) executes at most. When invoking a command, its handler method passes the argument values from the specified value sources (these are all of the parameters passed to SetHandler after the delegate). In this case, the values are sourced from the strings passed on the command line. Also, note that an additional parameter, IConsole, is passed to the handler method. IConsole is a built-in System.CommandLine type that abstracts access to the console (System.Console). You can provide your own type resolution by creating custom binders as well.

Helpful by Default

The capability to specify strongly-typed arguments for the options is great for the developer, but what about the end-user? The model binding within System.CommandLine attempts to convert the strings specified on the command line to the target argument type. This process should feel familiar to anyone familiar with model binding in ASP.NET Core. For example, these commands produce the same result (at least for my machine when writing this).

> .\LiveFile.exe --countdown-to "3/16/2021 21:00" --verbosity Detailed
> .\LiveFile.exe --countdown-to 21:00 --verbosity 2

You can read about the various model binding rules in the documentation; however, System.CommandLine attempts to follow the principle of least surprise. In most cases, things work as expected.

In the LiveFile help output above, there are a couple of option additions; a version option and several aliases for a help option add automatically. System.CommandLine comes with many valuable features enabled by default. As you may have guessed, the help option displays the help output for the command. Likewise, the version option displays the current version of the tool.

In particular, the help option has a lot of customization available. It allows for changing the displayed information for individual items, replacing entire sections, or completely replacing the help output with a custom implementation. The help option can augment its output with libraries like Spectre.Console to make your apps look great.

Another feature enabled by default is suggestions (tab completion) for end-users. Though the app enables this feature by default, it does require the end-users to take an additional step and add a script to their shell profile. However, once this step completes one time, all applications using System.CommandLine include working suggestions (including the dotnet CLI).

System.CommandLine suggestions for end-users

Making Simple Apps Easy

The code shown so far builds on top of the core System.CommandLine library. Though the core library strives to have a simple unopinionated API, many users may want to extend it to provide alternate ways to configure their command-line interface. Within System.CommandLine, these extensions are called flavors (previously called application models). One such example is the code-named Dragon Fruit, a flavor also built by the System.CommandLine team.

This flavor aims to make building CLI apps containing only a root command with options (a common use case) as straightforward as possible. Let’s take a quick look at what it takes to convert the LiveFile app over to it. First, change the NuGet reference from System.CommandLine to System.CommandLine.DragonFruit.

System.CommandLine.DragonFruit

Next, move the entire application into the Main method within a Program class.

using System.CommandLine;
using System.CommandLine.IO;

namespace LiveFile;
public class Program
{
    /// <summary>
    /// A countdown timer application
    /// </summary>
    /// <param name="countdownTo">The target time to count down to</param>
    /// <param name="outputFile">The output file to write to</param>
    /// <param name="verbosity">The verbosity of the output</param>
    /// <param name="console">The console</param>
    /// <returns></returns>
    public static async Task Main(DateTime countdownTo, FileInfo? outputFile, Verbosity verbosity, IConsole console)
    {
        if (outputFile is null)
        {
            outputFile = new FileInfo("output.txt");
        }
        //...
    }
}

The complete code for this example is accessible in this GitHub repository.

This is not a regular C# Main method. Rather than taking a string array as its parameter, it takes our options’ argument types. DragonFruit creates a command-line interface from this main method like our first example. As a result of its simplicity, DragonFruit cannot expose all the same features in the core System.CommandLine library. In this case, it means there is no way to express the default value of “new FileInfo(“output.txt”)” for the outputFile parameter. Its default value must be handled within the Main method, which gets automatically set as the root command’s handler.

Looking at the help output, we can see that the command-line interface for our app remains nearly identical (missing only the default value for --output-file that was there previously).

> .\LiveFile --help
Description:
  A countdown timer application

Usage:
  LiveFile [options]

Options:
  --countdown-to <countdown-to>        The target time to count down to
  --output-file <output-file>          The output file to write to
  --verbosity <Detailed|Normal|Quiet>  The verbosity of the output
  --version                            Show version information
  -?, -h, --help                       Show help and usage 

DragonFruit pulls in the XML docs and uses the descriptions to populate the help output to support descriptions. This System.CommandLine flavor is a simple, intuitive, and elegant solution for building simple command-line applications.

There are many ways developers like to configure their command-line interfaces and System.CommandLine aims to lay a solid foundation where others can build the flavors they desire.

Wrapping Up

There are many more features of the System.CommandLine library that this blog did not cover: support for trimming, directives, validators, response files, built-in and custom middleware, cancellation, advanced model binding, subcommands, etc. Although the System.CommandLine NuGet packages are still in preview, the library is very stable, well tested, and actively used by many projects (including the dotnet CLI). Currently, the library is actively working towards getting to a full GA release. With each of these betas, the team would love to have you give it a try and provide back any feedback you have on it.

Want More?

Check out our Demystified article introducing coding conventions and why developing coding standards for your team matters. Looking for IntelliTect to assist you with your application architecture? Check out our software architecture expertise.

Leave a Reply

Your email address will not be published. Required fields are marked *