Declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. Includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments.
$ dotnet add package DotMake.CommandLine
Declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. Includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments.
System.CommandLine is a very good parser but you need a lot of boilerplate code to get going and the API is hard to discover. This becomes complicated to newcomers and also you would have a lot of ugly code in your Program.cs to maintain. What if you had an easy class-based layer combined with a good parser?
Install the library to your console app project with NuGet.
In your project directory, via dotnet cli:
dotnet add package DotMake.CommandLine
or in Visual Studio Package Manager Console:
PM> Install-Package DotMake.CommandLine
Create a simple class like this:
using System;
using DotMake.CommandLine;
[DotMakeCliCommand(Description = "A root cli command")]
public class RootCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
}In Program.cs, add this single line:
DotMakeCli.Run<RootCliCommand>(args);And that's it! You now have a fully working command-line app. You just specify the name of your class which represents your root command to DotMakeCli.Run<> method and everything is wired.
If you want to go async, just use this:
await DotMakeCli.RunAsync<RootCliCommand>(args);To handle exceptions, you just use a try-catch block:
try
{
DotMakeCli.Run<RootCliCommand>(args);
}
catch (Exception e)
{
Console.WriteLine(@"Exception in main: {0}", e.Message);
}System.CommandLine, by default overtakes your exceptions that are thrown in command handlers (even if you don't set an exception handler explicitly) but DotMake.CommandLine, by default allows the exceptions to pass through. However if you wish, you can easily use an exception handler by using configureBuilderdelegate parameter iike this:
DotMakeCli.Run<RootCliCommand>(args, builder =>
builder.UseExceptionHandler((e, context) => Console.WriteLine(@"Exception in command handler: {0}", e.Message))
);Mark the class with DotMakeCliCommand attribute to make it a CLI command.
Mark a property with DotMakeCliOption attribute to make it a CLI option.
Mark a property with DotMakeCliArgument attribute to make it a CLI argument.
Add a method with name Run or RunAsync to make it the handler for the CLI command. The method can have one of the following signatures:
void Run()
int Run()
async Task RunAsync()
async Task<int> RunAsync()
Optionally the method signature can have a System.CommandLine.Invocation.InvocationContext parameter:
Run(InvocationContext context)
RunAsync(InvocationContext context)
The signatures which return int value, sets the ExitCode of the app.
Call DotMakeCli.Run<> orDotMakeCli.RunAsync<> method with your class name to run your CLI app.
When the command handler is run, the properties for CLI options and arguments will be already populated and bound from values passed in the command-line. If no matching value is passed, the property will have its default value.
When you run the app via
TestApp.exe in project output path (e.g. in TestApp\bin\Debug\net6.0)dotnet runin project directory (e.g. in TestApp)You see this result:
Handler for 'TestApp.Commands.RootCliCommand' is run:
Value for Option1 property is 'DefaultForOption1'
Value for Argument1 property is 'DefaultForArgument1'As we set default values for properties in the class, the option and the argument were already populated (even when the user did not pass any values).
When you run,
TestApp.exe NewValueForArgument1 --option-1 NewValueForOption1or (note the double hyphen/dash which allows dotnet run to pass arguments to our actual application):
dotnet run -- NewValueForArgument1 --option-1 NewValueForOption1You see this result:
Handler for 'TestApp.Commands.RootCliCommand' is run:
Value for Option1 property is 'NewValueForOption1'
Value for Argument1 property is 'NewValueForArgument1'When you run the app via TestApp.exe -? or dotnet run -- -?, you see this usage help:
Description:
A root cli command
Usage:
TestApp [<argument-1>] [options]
Arguments:
<argument-1> Description for Argument1 [default: DefaultForArgument1]
Options:
-o, --option-1 <option-1> Description for Option1 [default: DefaultForOption1]
-v, --version Show version information
-?, -h, --help Show help and usage informationNote, how command/option/argument names, descriptions and default value are automatically populated.
By default, command/option/argument names are generated as follows;
First the following suffixes are stripped out from class and property names:
Then the names are converted to kebab-case, this can be changed by setting NameCasingConvention property of the DotMakeCliCommand attribute to one of the following values:
DotMakeCliCasingConvention.NoneDotMakeCliCasingConvention.LowerCaseDotMakeCliCasingConvention.UpperCaseDotMakeCliCasingConvention.TitleCaseDotMakeCliCasingConvention.PascalCaseDotMakeCliCasingConvention.CamelCaseDotMakeCliCasingConvention.KebabCaseDotMakeCliCasingConvention.SnakeCaseFor options, double hyphen/dash prefix is added to the name (e.g. --option), this can be changed by setting NamePrefixConvention (default: DoubleHyphen) property of the DotMakeCliCommand attribute to one of the following values:
DotMakeCliPrefixConvention.SingleHyphenDotMakeCliPrefixConvention.DoubleHyphenDotMakeCliPrefixConvention.ForwardSlashFor options, short-form alias with first letter (e.g. -o) is automatically added. This can be changed by setting ShortFormAutoGenerate (default: true) and ShortFormPrefixConvention (default: SingleHyphen) properties of the DotMakeCliCommand attribute.
For example, change the name casing and prefix convention:
using System;
using DotMake.CommandLine;
[DotMakeCliCommand(
Description = "A cli command with snake_case name casing and forward slash prefix conventions",
NameCasingConvention = DotMakeCliCasingConvention.SnakeCase,
NamePrefixConvention = DotMakeCliPrefixConvention.ForwardSlash,
ShortFormPrefixConvention = DotMakeCliPrefixConvention.ForwardSlash
)]
public class RootCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
}When you run the app via TestApp.exe -? or dotnet run -- -?, you see this usage help:
Description:
A cli command with snake_case name casing and forward slash prefix conventions
Usage:
TestApp [<argument_1>] [options]
Arguments:
<argument_1> Description for Argument1 [default: DefaultForArgument1]
Options:
/o, /option_1 <option_1> Description for Option1 [default: DefaultForOption1]
/v, /version Show version information
-?, -h, /help Show help and usage informationNote how even the default options version and help use the new prefix convention ForwardSlash. By the way, as help is a special option, which allows user to discover your app, we still add short-form aliases with other prefix to prevent confusion.
A command in command-line input is a token that specifies an action or defines a group of related actions. For example:
dotnet run, run is a command that specifies an action.dotnet tool install, install is a command that specifies an action, and tool is a command that specifies a group of related commands. There are other tool-related commands, such as tool uninstall, tool list, and tool update.The root command is the one that specifies the name of the app's executable. For example, the dotnet command specifies the dotnet.exe executable.
Most command-line apps support subcommands, also known as verbs. For example, the dotnet command has a run subcommand that you invoke by entering dotnet run.
Subcommands can have their own subcommands. In dotnet tool install, install is a subcommand of tool.
Defining sub-commands in DotMake.Commandline is very easy. We simply use nested classes to create a hierarchy:
[DotMakeCliCommand(Description = "A root cli command with nested children")]
public class WithNestedChildrenCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
[DotMakeCliCommand(Description = "A nested level 1 sub-command")]
public class Level1SubCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
[DotMakeCliCommand(Description = "A nested level 2 sub-command")]
public class Level2SubCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
}
}
}Just make sure you apply DotMakeCliCommand attribute to the nested classes as well.
Command hierarchy in above example is: WithNestedChildrenCliCommand -> Level1SubCliCommand -> Level2SubCliCommand
Another way to create hierarchy between commands, especially if you want to use standalone classes, is to use Parent property of DotMakeCliCommand attribute to specify typeof parent class:
[DotMakeCliCommand(Description = "A root cli command")]
public class RootCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
}
[DotMakeCliCommand(
Name = "Level1External",
Description = "An external level 1 sub-command",
Parent = typeof(RootCliCommand)
)]
public class ExternalLevel1SubCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
[DotMakeCliCommand(Description = "A nested level 2 sub-command")]
public class Level2SubCliCommand
{
[DotMakeCliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[DotMakeCliArgument(Description = "Description for Argument1")]
public string Argument1 { get; set; } = "DefaultForArgument1";
public void Run()
{
Console.WriteLine($@"Handler for '{GetType().FullName}' is run:");
Console.WriteLine($@"Value for {nameof(Option1)} property is '{Option1}'");
Console.WriteLine($@"Value for {nameof(Argument1)} property is '{Argument1}'");
Console.WriteLine();
}
}
}Command hierarchy in above example is: RootCliCommand -> ExternalLevel1SubCliCommand -> Level2SubCliCommand
The class that DotMakeCliCommand attribute is applied to,
Parentproperty is not set.Parent property is set.The properties for DotMakeCliCommand attribute:
An option is a named parameter that can be passed to a command. POSIX CLIs typically prefix the option name with two hyphens (--). The following example shows two options:
dotnet tool update dotnet-suggest --verbosity quiet --global
^---------^ ^------^As this example illustrates, the value of the option may be explicit (quiet for --verbosity) or implicit (nothing follows --global). Options that have no value specified are typically Boolean parameters that default to true if the option is specified on the command line.
For some Windows command-line apps, you identify an option by using a leading slash (/) with the option name. For example:
msbuild /version
^------^Both POSIX and Windows prefix conventions are supported. When you configure an option, you specify the option name including the prefix.
When manually setting a name (overriding target property's name), you should specify the option name including the prefix (e.g. --option, -option or /option)
The properties for DotMakeCliOption attribute:
An argument is a value passed to an option or a command. The following examples show an argument for the verbosity option and an argument for the build command.
dotnet tool update dotnet-suggest --verbosity quiet --global
^---^dotnet build myapp.csproj
^----------^Arguments can have default values that apply if no argument is explicitly provided. For example, many options are implicitly Boolean parameters with a default of true when the option name is in the command line. The following command-line examples are equivalent:
dotnet tool update dotnet-suggest --global
^------^
dotnet tool update dotnet-suggest --global true
^-----------^Some options have required arguments. For example in the .NET CLI, --output requires a folder name argument. If the argument is not provided, the command fails.
Arguments can have expected types, and System.CommandLine displays an error message if an argument can't be parsed into the expected type. For example, the following command errors because "silent" isn't one of the valid values for --verbosity:
dotnet build --verbosity silentCannot parse argument 'silent' for option '-v' as expected type 'Microsoft.DotNet.Cli.VerbosityOptions'. Did you mean one of the following?
Detailed
Diagnostic
Minimal
Normal
QuietThe properties for DotMakeCliArgument attribute: