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--name
is just to letdotnet cli
to 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.CommandLine
writing 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.