DotMake.CommandLine

repository·main·Indexed 41 days ago

https://github.com/dotmake-build/command-line

A source-generator-based library for building strongly-typed .NET command-line applications. It provides an attribute-driven model using [CliCommand], [CliArgument], and [CliOption] to define CLI structures via C# classes or delegates, reducing boilerplate compared to raw System.CommandLine. Supports .NET 8.0+, .NET Standard 2.0+, Native AOT, and dependency injection extensions.

Tokens
16.4K
Snippets
46
Records
57
Agent score
48%

What's inside DotMake.CommandLine

  1. Overview of DotMake.CommandLine

    main

    DotMake.CommandLine is a framework for building modern .NET command-line applications using strongly-typed C#. It provides an attribute-based, source-generated programming model that allows you to define commands, options, and arguments using classes, properties, and attributes.

    Key features include:

    • Source Generation: Handles CLI plumbing at compile time, eliminating the need for runtime reflection.
    • Built on System.CommandLine: Leverages the robust parsing capabilities of System.CommandLine while reducing boilerplate.
    • Modern .NET Support: Supports trimming, Native AOT compilation, and dependency injection out of the box.
  2. Understand how model binding determines required vs optional options

    main

    DotMake automatically populates properties of your command classes based on command-line input. The framework determines if an option or argument is required or optional based on the following rules:

    Required

    An option/argument is required if:

    • It is a reference type (e.g., string) with no property initializer.
    • It is a nullable value type (e.g., bool?) with no property initializer.
    • It is initialized with null or null! (e.g., public string Arg { get; set; } = null!;).
    • It is explicitly marked via the Required = true property in the [CliArgument] or [CliOption] attribute.
    • It uses the C# required modifier (e.g., public required string Opt { get; set; }).

    Optional

    An option/argument is optional if:

    • It is a value type (e.g., bool, int, enum) with no property initializer, as these have non-null default values.
    • It has a non-null property initializer (e.g., public string Arg { get; set; } = "Default";).
    • It is explicitly marked via [CliArgument(Required = false)] or [CliOption(Required = false)].

    Note: When using Required = false for reference types, you should check for null in your Run method to determine if the user omitted the option.

    public string Opt1 { get; set; } // => Required because default value is null
    
    [CliOption]
    public bool Opt2 { get; set; } // => Optional because default value is non-null
    
    [CliOption]
    public bool? Opt3 { get; set; } // => Required because default value is null
    
    [CliOption(Required = false)]
    public string Opt1 { get; set; } // => Optional because attribute has `Required = false`
  3. Understand arguments in DotMake Command-Line

    main

    In DotMake Command-Line, an argument is a value passed to either an option or a command.

    • Option Arguments: Values that modify an option (e.g., providing a level of verbosity to --verbosity).
    • Command Arguments: Values that act as targets for a command (e.g., providing a file path to a build command).

    Arguments can have default values. For many Boolean options, simply including the option name on the command line is equivalent to passing true as an argument.

    # Argument for the 'verbosity' option
    dotnet tool update dotnet-suggest --verbosity quiet
    
    # Argument for the 'build' command
    dotnet build myapp.csproj
  4. Use response files to manage long command-line inputs

    main

    A response file is a text file containing a set of tokens (commands, options, and arguments) for a command-line application. This feature, provided by System.CommandLine, is useful for:

    1. Bypassing terminal character limits: When the input string is too long for the terminal to handle.
    2. Command reuse: Invoking the same complex command repeatedly without retyping the entire line.

    To use a response file, prefix the filename with an @ symbol at the position in the command line where you want the file's contents to be inserted. While .rsp is a common convention, any file extension can be used.

    # Example 1: Full command in a file
    dotnet @sample1.rsp
    
    # Example 2: Partial command in a file
    dotnet build @sample2.rsp --output ./build-output/
  5. How the Delegate-based model works

    main

    The delegate-based model is ideal for simple applications. Instead of a class, you pass a lambda expression or a method reference directly to Cli.Run or Cli.RunAsync.

    Rules for Delegates:

    • Parameters in the delegate represent CLI options or arguments.
    • By default, a parameter is treated as a CLI option. Use [CliArgument] to change it to an argument.
    • You can use attributes like [CliOption] or [CliCommand] on the delegate itself (requires <LangVersion>10.0</LangVersion> if target framework is below .NET 6.0).
    • Set default values for parameters to make them optional.
    • Delegates can be async and return void, int, Task, or Task<int>.
    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}'");
    });
  6. How help output is generated from Assembly metadata

    main

    The header of the help output is automatically populated from your project's assembly attributes:

    1. First Line (Product & Version): Uses AssemblyProductAttribute (or <Product> in .csproj) and AssemblyInformationalVersionAttribute, AssemblyFileVersionAttribute, or AssemblyVersionAttribute (or <InformationalVersion>, <FileVersion>, or <Version> in .csproj).
    2. Second Line (Copyright): Uses AssemblyCopyrightAttribute (or <Copyright> in .csproj).
    3. Third Line (Description): Uses AssemblyDescriptionAttribute (or <Description> in .csproj) for root commands. For subcommands, it uses the [CliCommand].Description property.
  7. How the Class-based model works

    main

    The class-based model is recommended for complex applications requiring sub-commands or command inheritance.

    In this model, you define a class representing your command. DotMake.CommandLine uses source generators to turn your class and its properties into CLI commands, options, and arguments at compile time. At runtime, it parses the arguments and binds them to your class properties.

    Key Workflow:

    1. Mark the command class with [CliCommand].
    2. Define properties for arguments using [CliArgument] and options using [CliOption].
    3. Implement a handler method named Run or RunAsync.
    4. Execute the application using Cli.Run<T>(args) or Cli.RunAsync<T>(args).
    using System;
    using DotMake.CommandLine;
    
    // Run your CLI app:
    Cli.Run<RootCliCommand>(args);
    
    // Define your root command:
    [CliCommand(Description = "A root cli command")]
    public class RootCliCommand
    {
        [CliArgument(Description = "Description for Argument1")]
        public string Argument1 { get; set; } = "DefaultForArgument1";
    
        [CliOption(Description = "Description for Option1")]
        public bool Option1 { get; set; };
     
        public void Run()
        {
            Console.WriteLine($"Value for {nameof(Argument1)} property is '{Argument1}'");
            Console.WriteLine($"Value for {nameof(Option1)} property is '{Option1}'");
        }
    }
  8. Understand command-line option syntax and delimiters

    main

    DotMake supports both POSIX (--option, -o) and Windows (/option) prefix conventions.

    Delimiters

    You can use a space, =, or : to separate an option name from its argument:

    • myapp -v quiet
    • myapp -v=quiet
    • myapp -v:quiet

    For single-character option aliases, the delimiter can be omitted (e.g., myapp -vquiet).

    Option Bundling (Stacking)

    Single-character options can be bundled together after a single hyphen. For example, -a, -b, and -c can be written as -abc. Note that only the last option in a bundle can specify an argument. If an explicit option named -abc exists, it takes precedence over the bundle.

    # POSIX style
    dotnet tool update dotnet-suggest --verbosity quiet --global
    
    # Windows style
    msbuild /version
    
    # Delimiter variations
    dotnet build -v quiet
    dotnet build -v=quiet
    dotnet build -v:quiet
    
    # Bundling
    myapp -abc
  9. What are directives in System.CommandLine

    main

    Directives are syntactic elements in System.CommandLine that provide cross-cutting functionality across different command-line applications. They are distinct from the application's own commands and options, allowing them to apply universally.

    Syntax Rules:

    • They must appear after the application name but before any subcommands or options.
    • They must be enclosed in square brackets (e.g., [directive]).
    • They cannot contain spaces.
    • They can include an argument using a colon separator: [directive:value].
    • Unrecognized directives are ignored without causing parsing errors.
  10. Handle required and typed arguments

    main

    DotMake Command-Line supports strict argument validation:

    1. Required Arguments: Some options require an argument to function. If the argument is missing, the command will fail.
    2. Type Validation: Arguments can have expected types. If a provided argument cannot be parsed into the expected type (e.g., an invalid enum value), System.CommandLine will display a parsing error listing the valid options.

    Example error when an invalid value is provided for a typed option:

    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
  11. Implement command inheritance and option sharing

    main

    To avoid repeating common options and arguments across multiple commands, you can use C# class inheritance or interfaces.

    • Class Inheritance: Define common options in an abstract base class. Derived command classes will inherit these options and their command handlers (Run or RunAsync).
    • Interface Implementation: Use interfaces to group sets of options. Since C# supports multiple interface implementation, you can compose a command from several different option groups.

    Overriding Behavior: If a derived class provides its own property attribute or property initializer for a member inherited from a base class or interface, the derived class's version (the attribute and the default value) will be used by the CLI.

    [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; }
    }
  12. Implement required option groups

    main

    To enforce that a user provides exactly one option from a specific set, use the RequiredGroups property on the [CliCommand] attribute.

    1. Assign a common Group name to the related options using [CliOption(Group = "...")].
    2. Add that group name to the RequiredGroups array in the [CliCommand] attribute.

    If no option from the required group is provided, the CLI will display an error.

    [CliCommand(RequiredGroups = new[] { "auth" })]
    public class ReportCommand
    {
        // Mutually exclusive group (not required, but only one allowed)
        [CliOption(Group = "output-format")]
        public bool Json { get; set; }
    
        [CliOption(Group = "output-format")]
        public bool Xml { get; set; }
    
        // Required group (exactly one must be provided)
        [CliOption(Group = "auth")]
        public string ApiKey { get; set; }
    
        [CliOption(Group = "auth")]
        public string Token { get; set; }
        
        public void Run(CliContext context)
        {
            context.ShowValues();
        }
    }