CliCommandAttribute Class

Specifies a class that represents a command which is a specific action that the command line application performs.
C#
[CliCommand]
public class SomeCliCommand
The class that this 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.

Commands: 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, 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.

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.

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 !

Definition

Namespace: DotMake.CommandLine
Assembly: DotMake.CommandLine (in DotMake.CommandLine.dll) Version: 1.8.7
C#
public class CliCommandAttribute : Attribute
Inheritance
Object    Attribute    CliCommandAttribute

Example

C#
// Class-based model
// Create a simple class like this:

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

        //In Program.cs, add this single line:
        Cli.Run<RootCliCommand>(args);

        //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>();
C#
// 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 OutputFile { get; set; }

    [CliOption]
    public List<string> Lines { get; set; }

    public void Run()
    {
        if (OutputFile.Exists)
            return;

        using (var streamWriter = OutputFile.CreateText())
        {
            foreach (var line in Lines)
            {
                streamWriter.WriteLine(line);
            }
        }
    }
}
C#
// Any type with a public constructor or a static `Parse` method with a string parameter (other parameters, if any,
// should be optional) - These types can be bound/parsed automatically even if they are wrapped
// with `Enumerable` or `Nullable` type.

[CliCommand]
public class ArgumentConverterCliCommand
{
    [CliOption(Required = false)]
    public ClassWithConstructor Opt { get; set; }

    [CliOption(Required = false, AllowMultipleArgumentsPerToken = true)]
    public ClassWithConstructor[] OptArray { get; set; }

    [CliOption(Required = false)]
    public CustomStruct? OptNullable { get; set; }

    [CliOption(Required = false)]
    public IEnumerable<ClassWithConstructor> OptEnumerable { get; set; }

    [CliOption(Required = false)]
    public List<ClassWithConstructor> OptList { get; set; }

    [CliOption(Required = false)]
    public CustomList<ClassWithConstructor> OptCustomList { get; set; }

    [CliArgument]
    public IEnumerable<ClassWithParser> Arg { get; set; }

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

public class ClassWithConstructor
{
    private readonly string value;

    public ClassWithConstructor(string value)
    {
        if (value == "exception")
            throw new Exception("Exception in ClassWithConstructor");

        this.value = value;
    }

    public override string ToString()
    {
        return value;
    }
}

public class ClassWithParser
{
    private string value;

    public override string ToString()
    {
        return value;
    }

    public static ClassWithParser Parse(string value)
    {
        if (value == "exception")
            throw new Exception("Exception in ClassWithParser");

        var instance = new ClassWithParser();
        instance.value = value;
        return instance;
    }
}

public struct CustomStruct
{
    private readonly string value;

    public CustomStruct(string value)
    {
        this.value = value;
    }

    public override string ToString()
    {
        return value;
    }
}
C#
// Arrays, lists, collections - any type that implements `IEnumerable<T>` and has a public constructor with a `IEnumerable<T>`
// or `IList<T>` parameter (other parameters, if any, should be optional).
// If type is generic `IEnumerable<T>`, `IList<T>`, `ICollection<T>` interfaces itself, array `T[]` will be used.
// If type is non-generic `IEnumerable`, `IList`, `ICollection` interfaces itself, array `string[]` will be used.

[CliCommand]
public class EnumerableCliCommand
{
    [CliOption(Required = false)]
    public IEnumerable<int> OptEnumerable { get; set; }

    [CliOption(Required = false)]
    public List<string> OptList { get; set; }

    [CliOption(Required = false, AllowMultipleArgumentsPerToken = true)]
    public FileAccess[] OptEnumArray { get; set; }

    [CliOption(Required = false)]
    public Collection<int?> OptCollection { get; set; }

    [CliOption(Required = false)]
    public HashSet<string> OptHashSet { get; set; }

    [CliOption(Required = false)]
    public Queue<FileInfo> OptQueue { get; set; }

    [CliOption(Required = false)]
    public CustomList<string> OptCustomList { get; set; }

    [CliArgument]
    public IList ArgIList { get; set; }

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

public class CustomList<T> : List<T>
{
    public CustomList(IEnumerable<T> items)
        : base(items)
    {
        if (items is IEnumerable<string> strings && strings.First() == "exception")
            throw new Exception("Exception in CustomList");
    }
}
C#
// For example, change the name casing and prefix convention:

[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; }
}
C#
// 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();
            }
        }
    }
}
C#
// 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();
        }
    }
}
C#
// 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 !

// 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.

[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; }
}
C#
// 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.

[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; }
}
C#
// 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:

[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();
        }
    }
}
C#
// In `[CliOption]` and `[CliArgument]` attributes;
// `ValidationRules` property allows setting predefined validation rules such as `ExistingFile`, `NonExistingFile`, `ExistingDirectory`,
// `NonExistingDirectory`, `ExistingFileOrDirectory`, `NonExistingFileOrDirectory`, `LegalPath`, `LegalFileName`, `LegalUri`, `LegalUrl`.
// Validation rules can be combined.
// `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, ValidationRules = CliValidationRules.ExistingDirectory)]
    public DirectoryInfo OptDir { 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();
    }
}

Constructors

CliCommandAttributeInitializes a new instance of the CliCommandAttribute class

Properties

Aliases Gets or sets the set of alternative strings that can be used on the command line to specify the command.

The aliases will be also displayed in usage help of the command line application.

Description Gets or sets the description of the command. This will be displayed in usage help of the command line application.
Hidden Gets or sets a value indicating whether the command is hidden.

You might want to support a command, option, or argument, but avoid making it easy to discover. For example, it might be a deprecated or administrative or preview feature. Use the Hidden property to prevent users from discovering such features by using tab completion or help.

Name Gets or sets the name of the command that will be used on the command line to specify the command. This will be displayed in usage help of the command line application.

If not set (or is empty/whitespace), the name of the class that this attribute is applied to, will be used to generate command name automatically: These suffixes will be stripped from the class name: RootCliCommand, RootCommand, SubCliCommand, SubCommand, CliCommand, Command, Cli. Then the name will be converted to kebab-case, for example:

  • If class name is Build or BuildCommand or BuildRootCliCommand -> command name will be build
  • If class name is BuildText or BuildTextCommand or BuildTextSubCliCommand -> command name will be build-text

Default convention can be changed via command's NameCasingConvention property.

NameCasingConvention Gets or sets the character casing convention to use for automatically generated command, option and argument names. This setting will be inherited by child options, child arguments and subcommands. This setting can be overriden by a subcommand in the inheritance chain.

Default is KebabCase (e.g. kebab-case).

NamePrefixConvention Gets or sets the prefix convention to use for automatically generated option names. This setting will be inherited by child options and subcommands. This setting can be overriden by a subcommand in the inheritance chain.

Default is DoubleHyphen (e.g. --option).

Parent Gets or sets the parent of the command. This property is used when you prefer to use a non-nested class for a subcommand, i.e. when you want to separate root command and subcommands into different classes/files. If the class that this attribute is applied to, is already a nested class, then this property will be ignored.
ShortFormAutoGenerate Gets or sets a value which indicates whether short form aliases are automatically generated for options. Short forms typically have a leading delimiter followed by a single character (e.g. -o or --o or /o). Default delimiter (e.g. -o) is changed via ShortFormPrefixConvention. This setting will be inherited by child options and subcommands. This setting can be overriden by a subcommand in the inheritance chain.

Default is .

ShortFormPrefixConvention Gets or sets the prefix convention to use for automatically generated short form option aliases. Short forms typically have a leading delimiter followed by a single character (e.g. -o or --o or /o). This setting will be inherited by child options and subcommands. This setting can be overriden by a subcommand in the inheritance chain.

Default is SingleHyphen (e.g. -o).

TreatUnmatchedTokensAsErrors Gets or sets a value that indicates whether unmatched tokens should be treated as errors. For example, if set to and an extra command or argument is provided, validation will fail.

Default is .

See Also