Cocona

repository·master·Indexed 25 days ago

https://github.com/mayuki/cocona

A micro-framework for .NET Core console applications to build CLI tools using an ASP.NET Core-like Minimal API or a class-based command structure. It supports dependency injection, shell completion for bash and zsh, data annotation validation, and a lightweight version called Cocona.Lite to minimize dependencies.

Tokens
5.3K
Snippets
21
Records
31
Agent score
86%

What's inside Cocona

  1. Create a console application using Class-based style

    master

    For .NET Standard 2.0, .NET 5, or older environments, use the class-based approach. You pass the class type to CoconaApp.Run<T>(args), and every public method in that class becomes a command available in the CLI.

    using Cocona;
    
    class Program
    {
        static void Main(string[] args)
        {
            // Cocona parses command-line and executes a command.
            CoconaApp.Run<Program>(args);
        }
    
        // public method as a command ™
        public void Hello(string name)
        {
            Console.WriteLine($"Hello {name}");
        }
    }
    using Cocona;
    class Program
    {
        static void Main(string[] args)
        {
            // Cocona parses command-line and executes a command.
            CoconaApp.Run<Program>(args);
        }
    
        // public method as a command ™
        public void Hello(string name)
        {
            Console.WriteLine($"Hello {name}");
        }
    }
  2. Validate options and arguments

    master

    Cocona supports validation using .NET Data Annotations (System.ComponentModel.DataAnnotations). You can use built-in attributes like [Range], [MaxLength], and [MinLength].

    To implement custom validation, inherit from ValidationAttribute and override IsValid.

  3. Create a console application with Cocona (Minimal API style)

    master

    For .NET 6 and later, you can use the Minimal API style to define your application logic directly within CoconaApp.Run. Parameters in the lambda expression are automatically mapped to command-line options or arguments.

    CoconaApp.Run((string? name, bool hey) =>
        Console.WriteLine($"{(hey ? "Hey" : "Hello")} {(name ?? "Guest")}!"));
  4. Enable shell command-line completion

    master

    Cocona supports tab completion for bash and zsh. This feature is disabled by default. To use it, set EnableShellCompletionSupport to true in your configuration.

    Once enabled, you can generate a completion script using the --completion built-in option:

    # For bash
    source <(./myapp --completion bash)
    
    # For zsh
    ./myapp --completion zsh > ~/.zsh/functions
    $ source <(./myapp --completion bash)
  5. Handle shutdown events and cancellation

    master

    To handle shutdown signals (like Ctrl+C), monitor the CancellationToken provided by the CoconaAppContext or via ICoconaAppContextAccessor.

    // Using CoconaAppContext directly
    app.AddCommand(async (CoconaAppContext ctx) =>
    {
        while (!ctx.CancellationToken.IsCancellationRequested)
        {
            await Task.Delay(100);
        }
    });
    
    // Using ICoconaAppContextAccessor (Dependency Injection style)
    public async Task RunAsync([FromService] ICoconaAppContextAccessor contextAccessor)
    {
        var ctx = contextAccessor.Current ?? throw new InvalidOperationException();
        while (!ctx.CancellationToken.IsCancellationRequested)
        {
            await Task.Delay(100);
        }
    }
  6. Integrate Cocona with GenericHost

    master

    You can integrate Cocona with Microsoft.Extensions.Hosting.GenericHost using the ConfigureCocona extension method. This allows you to manage Cocona commands within a standard .NET GenericHost lifecycle.

    class Program
    {
        static async Task Main(string[] args)
        {
            await Host.CreateDefaultBuilder()
                .ConfigureCocona(args, new[] { typeof(Program) })
                .Build()
                .RunAsync();
        }
    
        public void Hello()
        {
            Console.WriteLine($"Hello Konnichiwa!");
        }
    }
  7. Use Dependency Injection in commands

    master

    Cocona integrates with .NET Dependency Injection. If a command method or a class constructor has parameters, Cocona will attempt to inject instances from the IServiceProvider.

    You can also explicitly mark a parameter with [FromService] to ensure it is resolved from the DI container.

  8. Use Parameter Sets to define common parameters

    master

    To avoid redefining common parameters (like host or user) for every command, implement the ICommandParameterSet interface on a class or record. Cocona will automatically treat the members of this set as part of the command definition.

    Using a parameterized constructor (or record)

    If the class/record has a parameterized constructor, Cocona uses those parameters directly.

    Using properties (parameter-less constructor)

    If the class has a parameter-less constructor, mark public properties with [Option] or [Argument].

    Note: Options defined as properties are required by default. To make an option non-required with a default value, you must also use the [HasDefaultValue] attribute.

    public record CommonParameters(
        [Option('t', Description = "Specifies the remote host to connect.")]
        string Host,
        [Option('p', Description = "Port to connect to on the remote host.")]
        int Port,
        [Option('u', Description = "Specifies the user to log in as on the remote host.")]
        string User = "root",
        [Option('f', Description = "Perform without user confirmation.")]
        bool Force = false
    ) : ICommandParameterSet;
    
    // Usage in a command
    public void Add(CommonParameters commonParams, [Argument] string from, [Argument] string to)
        => Console.WriteLine($"{commonParams.User}@{commonParams.Host}");
  9. Localize commands using Microsoft.Extensions.Localization

    master

    You can localize command descriptions by registering Microsoft.Extensions.Localization and implementing ICoconaLocalizer. Note that MicrosoftExtensionLocalizationCoconaLocalizer is not included in the Cocona core library and must be added manually to the service collection.

    // Register Microsoft.Extensions.Localization and ICoconaLocalizer services
    var builder = CoconaApp.CreateBuilder();
    builder.Services.AddLocalization(options =>
    {
        options.ResourcesPath = "Resources";
    });
    
    // `MicrosoftExtensionLocalizationCoconaLocalizer` is not included in Cocona core library.
    builder.Services.TryAddTransient<ICoconaLocalizer, MicrosoftExtensionLocalizationCoconaLocalizer>();
    
    var app = builder.Build();
    app.AddCommand("hello", ([Argument(Description = "Name")]string name, IStringLocalizer<Program> localizer) =>
        {
            // Get a localized text from Microsoft.Extensions.Localization.IStringLocalizer
            Console.WriteLine(localizer.GetString("Hello {0}!", name));
        })
        .WithDescription("Say Hello");
    app.Run();
  10. Publish Cocona application as a single-file executable

    master

    If your application runs on .NET Core 3.0 or later, you can publish it as a single-file executable using the dotnet publish command with the PublishSingleFile property set to true.

    Windows (x64):

    dotnet publish -r win-x64 -p:PublishSingleFile=true

    Linux (x64):

    dotnet publish -r linux-x64 -p:PublishSingleFile=true
    dotnet publish -r win-x64 -p:PublishSingleFile=true
  11. Install Cocona or Cocona.Lite via NuGet

    master

    You can install the standard Cocona package or the lightweight Cocona.Lite version if you want to minimize dependencies. Use the following commands to add the package to your project:

    Standard Cocona:

    dotnet add package Cocona

    Cocona.Lite:

    dotnet add package Cocona.Lite
    dotnet add package Cocona
  12. Use Cocona.Lite for lightweight applications

    master

    If you want to avoid Microsoft.Extensions.* dependencies (Logging, DI, Configuration), use Cocona.Lite. It provides almost the same features and APIs as Cocona but with fewer overheads and minimal Dependency Injection. Use the CoconaLiteApp class instead of CoconaApp.

    $ dotnet add package Cocona.Lite
    // Option 1: Run with an action
    CoconaLiteApp.Run(() => { ... });
    
    // Option 2: Manual creation and command addition
    var app = CoconaLiteApp.Create();
    app.AddCommand(() => { ... });
    app.Run();
    
    // Option 3: Using a Program class
    static void Main(string[] args)
    {
        CoconaLiteApp.Run<Program>(args);
    }