command-line

DotMake Command-Line Logo

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, AOT compilation and dependency injection!

Nuget

DotMake Command-Line Intro

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

Usage

Delegate-based model

Create a CLI App with DotMake.Commandline in seconds!

In Program.cs, add this simple code:

using System;
using DotMake.CommandLine;

Cli.Run(([CliArgument]string arg1, bool opt1) =>
{
    Console.WriteLine($@"Value for {nameof(arg1)} parameter is '{arg1}'");
    Console.WriteLine($@"Value for {nameof(opt1)} parameter is '{opt1}'");
});

And that’s it! You now have a fully working command-line app.

Summary

Class-based model

While delegate-based model above is useful for simple apps, for more complex apps, you should use the class-based model because you can have sub-commands and command inheritance.

In Program.cs, add this simple code:

using System;
using DotMake.CommandLine;

// Add this single line to run you app!
Cli.Run<RootCliCommand>(args);

// Create a simple class like this to define your root command:
[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; }
 
    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();
    }
}

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.

args is the string array typically passed to a program. This is usually the special variable args available in Program.cs (new style with top-level statements) or the string array passed to the program’s Main method (old style). We also have method signatures which does not require args, for example you can also call Cli.Run<RootCliCommand>() and in that case args will be retrieved automatically from the current process via Cli.GetArgs().

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 the default exception handler by passing a CliSettings instance like below. Default exception handler prints the exception in red color to console:

Cli.Run<RootCliCommand>(args, new CliSettings { EnableDefaultExceptionHandler = true });

If you need to simply parse the command-line arguments without invocation, use this:

var parseResult = Cli.Parse<RootCliCommand>(args);
var rootCliCommand = parseResult.Bind<RootCliCommand>();

If you need to examine the parse result, such as errors:

var parseResult = Cli.Parse<RootCliCommand>(args);
if (parseResult.Errors.Count > 0)
{

}

Summary

Commands

A command in command-line input is a token that specifies an action or defines a group of related actions. For example:

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.

Command Hierarchy

Defining sub-commands in DotMake.Commandline is very easy. We simply use nested classes to create a hierarchy. Just make sure you apply CliCommand attribute to the nested classes as well. Command hierarchy in below example is:
RootWithNestedChildrenCliCommand -> Level1SubCliCommand -> Level2SubCliCommand

[CliCommand(Description = "A root cli command with nested children")]
public class RootWithNestedChildrenCliCommand
{
    [CliOption(Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultForOption1";

    [CliArgument(Description = "Description for Argument1")]
    public string Argument1 { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }

    [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; }

        public void Run(CliContext context)
        {
            context.ShowValues();
        }

        [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; }

            public void Run(CliContext context)
            {
                context.ShowValues();
            }
        }
    }
}

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. Consider you have this root command:

[CliCommand(Description = "A root cli command with external children and one nested child and testing settings inheritance")]
public class RootWithExternalChildrenCliCommand
{
    [CliOption(Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultForOption1";

    [CliArgument(Description = "Description for Argument1")]
    public string Argument1 { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }

    [CliCommand(
        Description = "A nested level 1 sub-command with custom settings, throws test exception",
        NameCasingConvention = CliNameCasingConvention.SnakeCase,
        NamePrefixConvention = CliNamePrefixConvention.ForwardSlash,
        ShortFormPrefixConvention = CliNamePrefixConvention.ForwardSlash
    )]
    public class Level1SubCliCommand
    {
        [CliOption(Description = "Description for Option1")]
        public string Option1 { get; set; } = "DefaultForOption1";

        [CliArgument(Description = "Description for Argument1")]
        public string Argument1 { get; set; }

        public void Run()
        {
            throw new Exception("This is a test exception from Level1SubCliCommand");
        }
    }
}

Command hierarchy in below example is:
RootWithExternalChildrenCliCommand -> ExternalLevel1SubCliCommand -> Level2SubCliCommand

[CliCommand(
    Description = "An external level 1 sub-command",
    Parent = typeof(RootWithExternalChildrenCliCommand)
)]
public class ExternalLevel1SubCliCommand
{
    [CliOption(Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultForOption1";

    [CliArgument(Description = "Description for Argument1")]
    public string Argument1 { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }

    [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; }

        public void Run(CliContext context)
        {
            context.ShowValues();
        }
    }
}

Command hierarchy in below example is:
RootWithExternalChildrenCliCommand -> Level1SubCliCommand -> ExternalLevel2SubCliCommand -> Level3SubCliCommand

[CliCommand(
    Description = "An external level 2 sub-command",
    Parent = typeof(RootWithExternalChildrenCliCommand.Level1SubCliCommand),
    NameCasingConvention = CliNameCasingConvention.SnakeCase,
    NamePrefixConvention = CliNamePrefixConvention.ForwardSlash,
    ShortFormPrefixConvention = CliNamePrefixConvention.ForwardSlash
)]
public class ExternalLevel2SubCliCommand
{
    [CliOption(Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultForOption1";

    [CliArgument(Description = "Description for Argument1")]
    public string Argument1 { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }

    [CliCommand(Description = "A nested level 3 sub-command")]
    public class Level3SubCliCommand
    {
        [CliOption(Description = "Description for Option1")]
        public string Option1 { get; set; } = "DefaultForOption1";

        [CliArgument(Description = "Description for Argument1")]
        public string Argument1 { get; set; }

        public void Run(CliContext context)
        {
            context.ShowValues();
        }
    }
}

The class that CliCommand attribute is applied to,

Accessing parent commands

Sub-commands can get a reference to the parent command by adding a property of the parent command type.
Alternatively ParseResult.Bind<TDefinition> method can be called to manually get reference to a parent command.
Note that binding will be done only once per definition class, so calling this method consecutively for the same definition class will return the cached result.

// Sub-commands can get a reference to the parent command by adding a property of the parent command type.

[CliCommand(Description = "A root cli command with children that can access parent commands")]
public class ParentCommandAccessorCliCommand
{
    [CliOption(
        Description = "This is a global option (Recursive option on the root command), it can appear anywhere on the command line",
        Recursive = true)]
    public string GlobalOption1 { get; set; } = "DefaultForGlobalOption1";

    [CliArgument(Description = "Description for RootArgument1")]
    public string RootArgument1 { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }

    [CliCommand(Description = "A nested level 1 sub-command which accesses the root command")]
    public class Level1SubCliCommand
    {
        [CliOption(
            Description = "This is global for all sub commands (it can appear anywhere after the level-1 verb)",
            Recursive = true)]
        public string Level1RecursiveOption1 { get; set; } = "DefaultForLevel1RecusiveOption1";

        [CliArgument(Description = "Description for Argument1")]
        public string Argument1 { get; set; }

        // The parent command gets automatically injected
        public ParentCommandAccessorCliCommand RootCommand { get; set; }

        public void Run(CliContext context)
        {
            context.ShowValues();
        }

        [CliCommand(Description = "A nested level 2 sub-command which accesses its parent commands")]
        public class Level2SubCliCommand
        {
            [CliOption(Description = "Description for Option1")]
            public string Option1 { get; set; } = "DefaultForOption1";

            [CliArgument(Description = "Description for Argument1")]
            public string Argument1 { get; set; }

            // All ancestor commands gets injected
            public ParentCommandAccessorCliCommand RootCommand { get; set; }
            public Level1SubCliCommand ParentCommand { get; set; }

            public void Run(CliContext context)
            {
                context.ShowValues();

                Console.WriteLine();
                Console.WriteLine(@$"Level1RecursiveOption1 = {ParentCommand.Level1RecursiveOption1}");
                Console.WriteLine(@$"parent Argument1 = {ParentCommand.Argument1}");
                Console.WriteLine(@$"GlobalOption1 = {RootCommand.GlobalOption1}");
                Console.WriteLine(@$"RootArgument1 = {RootCommand.RootArgument1}");
            }
        }
    }
}

Command Inheritance

When you have repeating/common options and arguments for your commands, you can define them once in a base class and then share them by inheriting that base class in other command classes. Interfaces are also supported !

[CliCommand]
public class InheritanceCliCommand : CredentialCommandBase, IDepartmentCommand
{
    public string Department { get; set; } = "Accounting";
}

public abstract class CredentialCommandBase
{
    [CliOption(Description = "Username of the identity performing the command")]
    public string Username { get; set; } = "admin";

    [CliOption(Description = "Password of the identity performing the command")]
    public string Password { get; set; }

    public void Run()
    {
        Console.WriteLine($@"I am {Username}");
    }
}

public interface IDepartmentCommand
{
    [CliOption(Description = "Department of the identity performing the command (interface)")]
    string Department { get; set; }
}

The property attribute and the property initializer from the most derived class in the hierarchy will be used (they will override the base ones). The command handler (Run or RunAsync) will be also inherited. So in the above example, InheritanceCliCommand inherits options Username, Password from a base class and option Department from an interface. Note that the property initializer for Department is in the derived class, so that default value will be used.


The properties for CliCommand attribute (see CliCommandAttribute docs for more info):

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 manually setting a name (overriding decorated 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):

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):

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 if it has one or an error will be displayed if it’s a required option/argument and it was not specified on the command-line.

An option/argument will be considered required when

An option/argument will be considered optional when

Validation

In [CliOption] and [CliArgument] attributes; ValidationRules property allows setting predefined validation rules such as

Validation rules can be combined via using bitwise ‘or’ operator(| in C#).

ValidationPattern property allows setting a regular expression pattern for custom validation, and ValidationMessage property allows setting a custom error message to show when ValidationPattern does not match.

[CliCommand]
public class ValidationCliCommand
{
    [CliOption(Required = false, ValidationRules = CliValidationRules.ExistingFile)]
    public FileInfo OptFile1 { get; set; }

    [CliOption(Required = false, ValidationRules = CliValidationRules.NonExistingFile | CliValidationRules.LegalPath)]
    public string OptFile2 { get; set; }

    [CliOption(Required = false, ValidationPattern = @"(?i)^[a-z]+$")]
    public string OptPattern1 { get; set; }

    [CliOption(Required = false, ValidationPattern = @"(?i)^[a-z]+$", ValidationMessage = "Custom error message")]
    public string OptPattern2 { get; set; }

    [CliOption(Required = false, ValidationRules = CliValidationRules.LegalUrl)]
    public string OptUrl { get; set; }

    [CliOption(Required = false, ValidationRules = CliValidationRules.LegalUri)]
    public string OptUri { get; set; }

    [CliArgument(Required = false, ValidationRules = CliValidationRules.LegalFileName)]
    public string OptFileName { get; set; }

    public void Run(CliContext context)
    {
        context.ShowValues();
    }
}

Dependency Injection

Commands can have injected dependencies, this is supported via Microsoft.Extensions.DependencyInjection package (version >= 2.1.1). In your project directory, via dotnet cli:

dotnet add package Microsoft.Extensions.DependencyInjection

or in Visual Studio Package Manager Console:

PM> Install-Package Microsoft.Extensions.DependencyInjection

When the source generator detects that your project has reference to Microsoft.Extensions.DependencyInjection, it will generate extension methods for supporting dependency injection. For example, you can now add your services with the extension method Cli.Ext.ConfigureServices:

using DotMake.CommandLine;
using Microsoft.Extensions.DependencyInjection;

Cli.Ext.ConfigureServices(services =>
{
    services.AddTransient<TransientClass>();
    services.AddScoped<ScopedClass>();
    services.AddSingleton<SingletonClass>();
});

Cli.Run<RootCliCommand>();

Then let them be injected to your command class automatically by providing a constructor with the required services:

[CliCommand(Description = "A root cli command with dependency injection")]
public class RootCliCommand
{
    private readonly TransientClass transientDisposable;
    private readonly ScopedClass scopedDisposable;
    private readonly SingletonClass singletonDisposable;

    public RootCliCommand(
        TransientClass transientDisposable,
        ScopedClass scopedDisposable,
        SingletonClass singletonDisposable
    )
    {
        this.transientDisposable = transientDisposable;
        this.scopedDisposable = scopedDisposable;
        this.singletonDisposable = singletonDisposable;
    }

    [CliOption(Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultForOption1";

    [CliArgument(Description = "Description for Argument1")]
    public string Argument1 { get; set; }

    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();

        Console.WriteLine($"Instance for {transientDisposable.Name} is available");
        Console.WriteLine($"Instance for {scopedDisposable.Name} is available");
        Console.WriteLine($"Instance for {singletonDisposable.Name} is available");
        Console.WriteLine();
    }
}

public sealed class TransientClass : IDisposable
{
    public string Name => nameof(TransientClass);

    public void Dispose() => Console.WriteLine($"{nameof(TransientClass)}.Dispose()");
}

public sealed class ScopedClass : IDisposable
{
    public string Name => nameof(ScopedClass);

    public void Dispose() => Console.WriteLine($"{nameof(ScopedClass)}.Dispose()");
}

public sealed class SingletonClass : IDisposable
{
    public string Name => nameof(SingletonClass);

    public void Dispose() => Console.WriteLine($"{nameof(SingletonClass)}.Dispose()");
}

Other dependency injection containers (e.g. Autofac) are also supported via Microsoft.Extensions.DependencyInjection.Abstractions package (version >= 2.1.1). In your project directory, via dotnet cli:

dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions

or in Visual Studio Package Manager Console:

PM> Install-Package Microsoft.Extensions.DependencyInjection.Abstractions

When the source generator detects that your project has reference to Microsoft.Extensions.DependencyInjection.Abstractions, it will generate extension methods for supporting custom service providers. For example, you can now set your custom service provider with the extension method Cli.Ext.SetServiceProvider:

using DotMake.CommandLine;
using Autofac.Core;
using Autofac.Core.Registration;

var cb = new ContainerBuilder();
cb.RegisterType<object>();
var container = cb.Build();

Cli.Ext.SetServiceProvider(container);

Cli.Run<RootCliCommand>();

Help output

When you run the app via

A root cli command

Usage: TestApp [options]

Arguments:

Description for Argument1 [required] Options: -o, --option-1 Description for Option1 [default: DefaultForOption1] -v, --version Show version information -?, -h, --help Show help and usage information ``` - First line comes from `AssemblyProductAttribute` or `AssemblyName` (`` tag in your .csproj file). Version comes from `AssemblyInformationalVersionAttribute` or `AssemblyFileVersionAttribute` or `AssemblyVersionAttribute` (`` or `` or `` tag in your .csproj file). - Second line comes from `AssemblyCopyrightAttribute` (`` tag in your .csproj file). - Third line comes from `Description` property of `[CliCommand]` or for root commands, `AssemblyDescriptionAttribute` (`` tag in your .csproj file). 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 the `CliCommand` 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 setting `NamePrefixConvention` (default: DoubleHyphen) property of the `CliCommand` 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 setting `ShortFormAutoGenerate` (default: true) and `ShortFormPrefixConvention` (default: SingleHyphen) properties of the `CliCommand` attribute. --- For example, change the name casing and prefix convention: ```c# 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 RootSnakeSlashCliCommand { [CliOption(Description = "Description for Option1")] public string Option1 { get; set; } = "DefaultForOption1"; [CliArgument(Description = "Description for Argument1")] public string Argument1 { get; set; } 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: ```console DotMake Command-Line TestApp v1.6.0 Copyright © 2023-2024 DotMake A cli command with snake_case name casing and forward slash prefix conventions Usage: TestApp [options] Arguments: Description for Argument1 [required] Options: /o, /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. ### Themes Cli app theme can be changed via setting `CliSettings.Theme` property to predefined themes Red, DarkRed, Green, DarkGreen, Blue, DarkBlue or a custom `CliTheme`. These color and formatting option are mainly used by the help output. ```c# Cli.Run(args, new CliSettings { Theme = CliTheme.Red }); Cli.Run(args, new CliSettings { Theme = CliTheme.DarkRed }); Cli.Run(args, new CliSettings { Theme = CliTheme.Green }); Cli.Run(args, new CliSettings { Theme = CliTheme.DarkGreen }); Cli.Run(args, new CliSettings { Theme = CliTheme.Blue }); Cli.Run(args, new CliSettings { Theme = CliTheme.DarkBlue }); Cli.Run(args, new CliSettings { Theme = new CliTheme(CliTheme.Default) { HeadingCasing = CliNameCasingConvention.UpperCase, HeadingNoColon = true } }); ``` ### Localization Localizing commands, options and arguments is supported. You can specify a `nameof` operator expression with a resource property (generated by resx) in the attribute's argument (for `string` types only) and the source generator will smartly use the resource property accessor as the value of the argument so that it can localize at runtime. If the property in the `nameof` operator expression does not point to a resource property, then the name of that property will be used as usual. The reason we use `nameof` operator is that attributes in `.NET` only accept compile-time constants and you get `CS0182` error if not, so specifying resource property directly is not possible as it's not a compile-time constant but it's a static property access. ```c# [CliCommand(Description = nameof(TestResources.CommandDescription))] internal class LocalizedCliCommand { [CliOption(Description = nameof(TestResources.OptionDescription))] public string Option1 { get; set; } = "DefaultForOption1"; [CliArgument(Description = nameof(TestResources.ArgumentDescription))] public string Argument1 { get; set; } 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(); } } ``` ### Triggering help If a command represents a group and not an action, you may want to show help. If `Run` or `RunAsync` method is missing in a command class, then by default it will show help. You can also manually trigger help in `Run` or `RunAsync` method of a command class via calling `CliContext.ShowHelp`. For testing a command, other methods `CliContext.ShowValues` and `CliContext.IsEmptyCommand` are also useful. `ShowValues` shows parsed values for current command and its arguments and options. See below example; root command does not have a handler method so it will always show help and sub-command will show help if command is specified without any arguments or option, and it will show (dump) values if not: ```c# [CliCommand(Description = "A root cli command")] public class HelpCliCommand { [CliOption(Description = "Description for Option1")] public string Option1 { get; set; } = "DefaultForOption1"; [CliArgument(Description = "Description for Argument1")] public string Argument1 { get; set; } = "DefaultForArgument1"; [CliCommand(Description = "A sub cli command")] public class SubCliCommand { [CliArgument(Description = "Description for Argument2")] public string Argument2 { get; set; } = "DefaultForArgument2"; public void Run(CliContext context) { if (context.IsEmptyCommand()) context.ShowHelp(); else context.ShowValues(); } } } ``` ## Additional documentation - [DotMake Command-Line API docs](https://dotmake.build/api/) - [Command-line syntax overview for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax)