DotMake.CommandLine
1.5.2
See the version list below for details.
dotnet add package DotMake.CommandLine --version 1.5.2
NuGet\Install-Package DotMake.CommandLine -Version 1.5.2
<PackageReference Include="DotMake.CommandLine" Version="1.5.2" />
paket add DotMake.CommandLine --version 1.5.2
#r "nuget: DotMake.CommandLine, 1.5.2"
// Install DotMake.CommandLine as a Cake Addin #addin nuget:?package=DotMake.CommandLine&version=1.5.2 // Install DotMake.CommandLine as a Cake Tool #tool nuget:?package=DotMake.CommandLine&version=1.5.2
DotMake Command-Line
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?
DotMake.CommandLine is a library which provides declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. The library includes includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments. Supports trimming and AOT compilation !
Getting started
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
Prerequisites
- .NET 6.0 and later project or .NET Standard 2.0 and later project (note that .NET Framework 4.7.2+ can reference netstandard2.0 libraries).
- Visual Studio 2022 v17.3+ or .NET SDK 6.0.407+ (our incremental source generator requires performance features added first in these versions).
- Usually a console app project but you can also use a class library project which will be consumed later.
Usage
Create a simple class like this:
using System;
using DotMake.CommandLine;
[CliCommand(Description = "A root cli command")]
public class RootCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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:
Cli.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 Cli.Run<>
method and everything is wired.
If you want to go async, just use this:
await Cli.RunAsync<RootCliCommand>(args);
To handle exceptions, you just use a try-catch block:
try
{
Cli.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 configureBuilder
delegate parameter like this:
Cli.Run<RootCliCommand>(args, builder =>
builder.UseExceptionHandler((e, context) => Console.WriteLine(@"Exception in command handler: {0}", e.Message))
);
If you need to simply parse the command-line arguments without invocation, use this:
var rootCliCommand = Cli.Parse<RootCliCommand>(args);
If you need to examine the parse result, such as errors:
var rootCliCommand = Cli.Parse<RootCliCommand>(args, out var parseResult);
if (parseResult.Errors.Count > 0)
{
}
Summary
Mark the class with
CliCommand
attribute to make it a CLI command (see CliCommandAttribute docs for more info).Mark a property with
CliOption
attribute to make it a CLI option (see CliOptionAttribute docs for more info).Mark a property with
CliArgument
attribute to make it a CLI argument (see CliArgumentAttribute docs for more info).Add a method with name
Run
orRunAsync
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 in case you need to access it:
-
Run(InvocationContext context)
-
RunAsync(InvocationContext context)
The signatures which return int value, sets the ExitCode of the app.
-
Call
Cli.Run<>
orCli.RunAsync<>
method with your class name to run your CLI app (see Cli docs for more info).
Model binding
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. inTestApp\bin\Debug\net6.0
)- or
dotnet run
in project directory (e.g. inTestApp
)
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 NewValueForOption1
or (note the double hyphen/dash which allows dotnet run
to pass arguments to our actual application):
dotnet run -- NewValueForArgument1 --option-1 NewValueForOption1
You see this result:
Handler for 'TestApp.Commands.RootCliCommand' is run:
Value for Option1 property is 'NewValueForOption1'
Value for Argument1 property is 'NewValueForArgument1'
Supported types
Note that you can have a specific type (other than string
) for a property which a CliOption
or CliArgument
attribute is applied to, for example these properties will be parsed and bound/populated automatically:
[CliCommand]
public class WriteFileCommand
{
[CliArgument]
public FileInfo InputFile { get; set; };
[CliOption]
public List<string> Lines { get; set; };
}
The following types for properties is supported:
Booleans (flags) - If
true
orfalse
is passed for an option having abool
argument, it is parsed and bound as expected. But an option whose argument type isbool
doesn't require an argument to be specified. The presence of the option token on the command line, with no argument following it, results in a value oftrue
.Enums - The values are bound by name, and the binding is case insensitive
Arrays and lists (any IEnumerable type)
Common CLR types:
string
,bool
FileSystemInfo
,FileInfo
,DirectoryInfo
int
,long
,short
,uint
,ulong
,ushort
double
,float
,decimal
byte
,sbyte
DateTime
,DateTimeOffset
,DateOnly
,TimeOnly
,TimeSpan
Guid
Uri
,IPAddress
,IPEndPoint
Any type with a public constructor or a static
Parse
method with a string parameter - These types can be bound/parsed automatically even if they are wrapped withEnumerable
orNullable
type (note that as of current version, custom class support when using AOT compilation is not stable but trimming works)[CliCommand] public class ArgumentConverterCliCommand { [CliOption] public ClassWithConstructor Opt { get; set; } [CliOption] public ClassWithConstructor[] OptArray { get; set; } [CliOption] public CustomStruct? OptNullable { get; set; } [CliOption] public IEnumerable<ClassWithConstructor> OptEnumerable { get; set; } [CliOption] public List<ClassWithConstructor> OptList { get; set; } [CliArgument] public IEnumerable<Sub.ClassWithParser> Arg { get; set; } public void Run() { Console.WriteLine($@"Handler for '{GetType().FullName}' is run:"); foreach (var property in GetType().GetProperties()) { var value = property.GetValue(this); if (value is IEnumerable enumerable) value = string.Join(", ", enumerable .Cast<object>() .Select(s => s.ToString()) ); Console.WriteLine($@"Value for {property.Name} property is '{value}'"); } Console.WriteLine(); } } public class ClassWithConstructor { private readonly string value; public ClassWithConstructor(string value) { this.value = value; } public override string ToString() { return value; } } public struct CustomStruct { private readonly string value; public CustomStruct(string value) { this.value = value; } public override string ToString() { return value; } } namespace Sub { public class ClassWithParser { private readonly string value; private ClassWithParser(string value) { this.value = value; } public override string ToString() { return value; } public static ClassWithParser Parse(string value) { return new ClassWithParser(value); } } }
Help output
When you run the app via TestApp.exe -?
or dotnet run -- -?
, you see this usage help:
DotMake Command-Line TestApp v1.4.0
Copyright © 2023 DotMake
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 information
Note, how command/option/argument names, descriptions and default values 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:
- For commands: "RootCliCommand", "RootCommand", "SubCliCommand", "SubCommand", "CliCommand", "Command", "Cli"
- For options: "RootCommandOption", "SubCliCommandOption", "SubCommandOption", "CliCommandOption", "CommandOption", "CliOption", "Option"
- For arguments: "RootCliCommandArgument", "RootCommandArgument", "SubCliCommandArgument", "SubCommandArgument", "CliCommandArgument", "CommandArgument", "CliArgument", "Argument"
Then the names are converted to kebab-case, this can be changed by setting
NameCasingConvention
property of theCliCommand
attribute to one of the following values:CliNameCasingConvention.None
CliNameCasingConvention.LowerCase
CliNameCasingConvention.UpperCase
CliNameCasingConvention.TitleCase
CliNameCasingConvention.PascalCase
CliNameCasingConvention.CamelCase
CliNameCasingConvention.KebabCase
CliNameCasingConvention.SnakeCase
For options, double hyphen/dash prefix is added to the name (e.g.
--option
), this can be changed by settingNamePrefixConvention
(default: DoubleHyphen) property of theCliCommand
attribute to one of the following values:CliNamePrefixConvention.SingleHyphen
CliNamePrefixConvention.DoubleHyphen
CliNamePrefixConvention.ForwardSlash
For options, short-form alias with first letter (e.g.
-o
) is automatically added. This can be changed by settingShortFormAutoGenerate
(default: true) andShortFormPrefixConvention
(default: SingleHyphen) properties of theCliCommand
attribute.
For example, change the name casing and prefix convention:
using System;
using DotMake.CommandLine;
[CliCommand(
Description = "A cli command with snake_case name casing and forward slash prefix conventions",
NameCasingConvention = CliNameCasingConvention.SnakeCase,
NamePrefixConvention = CliNamePrefixConvention.ForwardSlash,
ShortFormPrefixConvention = CliNamePrefixConvention.ForwardSlash
)]
public class RootCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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 information
Note 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.
Command Hierarchy
A command in command-line input is a token that specifies an action or defines a group of related actions. For example:
- In
dotnet run
,run
is a command that specifies an action. - In
dotnet tool install
,install
is a command that specifies an action, andtool
is a command that specifies a group of related commands. There are other tool-related commands, such astool uninstall
,tool list
, andtool update
.
Root commands
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.
Subcommands
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:
[CliCommand(Description = "A root cli command with nested children")]
public class WithNestedChildrenCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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();
}
[CliCommand(Description = "A nested level 1 sub-command")]
public class Level1SubCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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();
}
[CliCommand(Description = "A nested level 2 sub-command")]
public class Level2SubCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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 CliCommand
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 CliCommand
attribute to specify typeof
parent class:
[CliCommand(Description = "A root cli command")]
public class RootCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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();
}
}
[CliCommand(
Name = "Level1External",
Description = "An external level 1 sub-command",
Parent = typeof(RootCliCommand)
)]
public class ExternalLevel1SubCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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();
}
[CliCommand(Description = "A nested level 2 sub-command")]
public class Level2SubCliCommand
{
[CliOption(Description = "Description for Option1")]
public string Option1 { get; set; } = "DefaultForOption1";
[CliArgument(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 CliCommand
attribute is applied to,
- will be a root command if the class is not a nested class and
Parent
property is not set. - will be a sub command if the class is a nested class or
Parent
property is set.
The properties for CliCommand
attribute (see CliCommandAttribute docs for more info):
- Name
- Description
- Aliases
- Hidden
- Parent
- TreatUnmatchedTokensAsErrors
- NameCasingConvention (inherited by child options, child arguments and subcommands)
- NamePrefixConvention (inherited by child options and subcommands)
- ShortFormPrefixConvention (inherited by child options and subcommands)
- ShortFormAutoGenerate (inherited by child options and subcommands)
Options
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 CliOption
attribute (see CliOptionAttribute docs for more info):
- Name
- Description
- Aliases
- HelpName
- Hidden
- Required
- Global
- Arity
- AllowedValues
- AllowMultipleArgumentsPerToken
Arguments
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 silent
Cannot parse argument 'silent' for option '-v' as expected type 'Microsoft.DotNet.Cli.VerbosityOptions'. Did you mean one of the following?
Detailed
Diagnostic
Minimal
Normal
Quiet
The properties for CliArgument
attribute (see CliArgumentAttribute docs for more info):
- Name
- Description
- HelpName
- Hidden
- Arity
- AllowedValues
Additional documentation
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.CommandLine (>= 2.0.0-beta4.22272.1)
-
net6.0
- System.CommandLine (>= 2.0.0-beta4.22272.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.8.8 | 1,823 | 7/19/2024 |
1.8.7 | 2,563 | 4/2/2024 |
1.8.6 | 467 | 3/6/2024 |
1.8.5 | 195 | 3/2/2024 |
1.8.4 | 218 | 2/29/2024 |
1.8.3 | 142 | 2/27/2024 |
1.8.2 | 131 | 2/26/2024 |
1.8.1 | 241 | 2/21/2024 |
1.8.0 | 269 | 2/13/2024 |
1.7.2 | 327 | 1/29/2024 |
1.7.0 | 293 | 1/25/2024 |
1.6.9 | 82 | 1/19/2024 |
1.6.8 | 279 | 1/16/2024 |
1.6.6 | 86 | 1/14/2024 |
1.6.4 | 82 | 1/10/2024 |
1.6.3 | 78 | 1/10/2024 |
1.6.2 | 110 | 1/9/2024 |
1.6.0 | 87 | 1/6/2024 |
1.5.9 | 94 | 1/4/2024 |
1.5.8 | 88 | 1/3/2024 |
1.5.7 | 94 | 1/1/2024 |
1.5.6 | 82 | 12/29/2023 |
1.5.5 | 73 | 12/27/2023 |
1.5.4 | 87 | 12/25/2023 |
1.5.2 | 66 | 12/23/2023 |
1.5.0 | 74 | 12/22/2023 |
1.4.0 | 83 | 12/19/2023 |
1.2.1 | 92 | 12/15/2023 |
1.2.0 | 88 | 12/15/2023 |
1.0.0 | 91 | 12/12/2023 |
- Changed behaviour: Added Required property to CliArgumentAttribute, default is true for arguments.
Default is false for options (already had Required property). Whether you specify a default value for a property will
no longer effect the requirement of the argument or option. By contrast, when you set Required to true, the default value
of the property will be ignored.
- Beautified help output: Moved [required] label to description (second column), prevented display of empty [] default values.
For wrong inputs, only errors will be shown and not the complete help output below.