System.CommandLine is a library provided by Microsoft which eases out the functionality needed by CLI. It provides a way to parse command-line input and display help text. It will also ensure that inputs are parsed in accordance with POSIX or Windows conventions.
🧑💻 Let’s build a Greeting CLI
We’ll be building a very minimalistic greeting application which will get the name as a user input and write greeting text on the console. We’ll be using .NET 6 and VS Code to create Console Application.
dotnet new console -o Greeting.Cli --framework net6.0
cd .\Greeting.Cli\
code .
Now, we have a .NET 6 console project opened in VS Code. Project only contains Greeting.Cli.csproj and Program.cs files. We’ll add a Greeting method in Program.cs
static void Greeting(string name) => Console.WriteLine($"Hello, {name}!");
We have a method which will take the name parameter and print a Hello {name}! on the console.
If we do dotnet run then we won’t see any output on the console as we have not called it from anywhere in code. Let’s add the sweetness of System.CommandLine to our CLI.
dotnet add package System.CommandLine --prerelease
This command will add the System.CommandLine package to the Greeting.Cli project, currently System.CommandLine is in beta so the --prerelease flag will be needed.
Now we need to update our Program.cs file to include the name option and root command.
using System.CommandLine;
static void Greeting(string name) => Console.WriteLine($"Hello, {name}!");
var nameOption = new Option<string>(
name: "--name",
description: "The person name to greet.");
var rootCommand = new RootCommand("Greeting CLI!");
rootCommand.AddOption(nameOption);
rootCommand.SetHandler((name) => Greeting(name), nameOption);
return await rootCommand.InvokeAsync(args);
If we again try to run our application with dotnet run we will see the Hello, ! in console output because we haven’t passed name parameter value from command-line, let’s pass --name value while running an app.
dotnet run -- --name Ashwini
--before--nameis just to letdotnet clito pass anything after--to console applications.
Here RootCommand represents the main action that the application performs, .AddOption will add --name option to rootCommand. In .SetHandler we can write our business logic. InvokeAsync will Parse and Invoke a command.
➡️ Add Short-form aliases and Required Option
Option supports multiple alias names, to add alias .AddAlias("-n") method will be used.
nameOption.AddAlias("-n");
Now, we can use -n instead of --name to provide name value.
PS D:\Greeting.Cli> dotnet run --name Ashwini
Hello, Ashwini!
PS D:\Greeting.Cli> dotnet run -n Ashwini
Hello, Ashwini!
and we will see expected output in console, previously we have seen if we do dotnet run then it will print Hello, ! this is because --name is an optional parameter, to make it required we can set nameOption.IsRequired = true;.
var nameOption = new Option<string>(
name: "--name",
description: "The person name to greet.");
nameOption.AddAlias("-n");
nameOption.IsRequired = true;
# Before setting --name as Required
PS D:\Greeting.Cli> dotnet run
Hello, !
# After setting --name as Required
PS D:\Greeting.Cli> dotnet run
Option '--name' is required.
Description:
Greeting CLI!
Usage:
Greeting.Cli [options]
Options:
-n, --name <name> (REQUIRED) The person's name to greet.
--version Show version information
-?, -h, --help Show help and usage information
📦 Build and Package our CLI
Till this point we are running our application with dotnet run, but we can use the executable created while building our application.
PS D:\Greeting.Cli\> cd .\bin\Debug\net6.0\
PS D:\Greeting.Cli\bin\Debug\net6.0> .\Greeting.Cli.exe --help
Description:
Greeting CLI!
Usage:
Greeting.Cli [options]
Options:
-n, -na, --name <name> (REQUIRED) The person's name to greet.
--version Show version information
-?, -h, --help Show help and usage information
PS D:\Greeting.Cli\bin\Debug\net6.0> .\Greeting.Cli.exe -n Ashwini
Hello, Ashwini!
We can publish our application as Self Contained or Framework dependent binaries. Or can publish using .NET global tools.
Add two lines in .csproj file to the end of the <PropertyGroup> node
<PackAsTool>true</PackAsTool>
<!-- <PackageOutputPath> is an optional element that determines where the NuGet package will be produced -->
<PackageOutputPath>./nupkgs</PackageOutputPath>
The .csproj now looks like the following -
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackAsTool>true</PackAsTool>
<PackageOutputPath>./nupkgs</PackageOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>
Now create a package and install it.
# Create Package
PS D:\Greeting.Cli> dotnet pack
Successfully created package 'D:\Greeting.Cli\nupkgs\Greeting.Cli.1.0.0.nupkg'.
# Install global tool from local directory
PS D:\Greeting.Cli> dotnet tool install --global --add-source ./nupkgs Greeting.Cli
You can invoke the tool using the following command: Greeting.Cli
Tool 'greeting.cli' (version '1.0.0') was successfully installed.
# Invoke program and pass options
PS D:\> Greeting.Cli --name Ash
Hello, Ash!
🤖 Add Sub-Commands
RootCommand is enough if your application is responsible for performing only one action, but if your application is performing more than one task then you need to have action specific commands.
Take an example of dotnet CLI which have multiple sub-commands to deal with different actions -
PS D:\Greeting.Cli> dotnet --help
...
SDK commands:
add Add a package or reference to a .NET project.
build Build a .NET project.
build-server Interact with servers started by a build.
clean Clean build outputs of a .NET project.
format Apply style preferences to a project or solution.
...
As of now our Greeting.Cli only have one action to perform then having a RootCommand is enough. Let’s extend the functionality of our CLI by accepting a template from the user, for that we’ll add a template sub-command to your CLI.
...
// -- Template Sub-Command --
var templateOption = new Option<string>(
name: "--template",
description: "The template use for greeting.");
templateOption.AddAlias("-t");
templateOption.IsRequired = true;
var templateCommand = new Command("template", "Print greeting in provided format.");
templateCommand.AddOption(nameOption);
templateCommand.AddOption(templateOption);
templateCommand.SetHandler((template, name) =>
{
var parsedString = string.Format(template, name);
Console.WriteLine(parsedString);
}, templateOption, nameOption);
// Add to RootCommand
rootCommand.AddCommand(templateCommand);
// -- Template Sub-Command end --
...
Here we initialized Command with command name and description and provided two options to it - name and template both as required. Also set the handler with parsing and print logic and at last we added it to the RootCommand instance.
If we see the help, it will show us the new template command
PS D:\Greeting.Cli> dotnet run -- --help
Description:
Greeting CLI!
Usage:
Greeting.Cli [command] [options]
Options:
-n, --name <name> (REQUIRED) The person name to greet.
--version Show version information
-?, -h, --help Show help and usage information
Commands:
template Print greeting in provided format.
PS D:\Greeting.Cli> dotnet run -- template --help
Description:
Print greeting in provided format.
Usage:
Greeting.Cli template [options]
Options:
-n, --name <name> (REQUIRED) The person's name to greet.
-t, --template <template> (REQUIRED) The template use for greeting.
-?, -h, --help Show help and usage information
Let’s use the template command and print the greeting in the provided format.
PS D:\Greeting.Cli> dotnet run -- template -t "Good Morning, {0}!" -n "Ashwini"
Good Morning, Ashwini!
Just like this we can create any number of sub-commands and nested sub-commands in our CLI.
💭 Closing Thoughts
- With the
System.CommandLinewriting console application which needs command-line arguments, it becomes so easy that all heavy-lifting is abstracted by the package so we can only focus on our business needs. - DeltaX Vault CLI is built using
System.CommandLine; Moving forward we shall write all of our utilities which require command-line arguments in similar fashion. - For Utilities deploy we must leverage the .NET global tool instead of copy-pasting binaries from one machine to another.