Remora.Commands 9.0.4

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package Remora.Commands --version 9.0.4                
NuGet\Install-Package Remora.Commands -Version 9.0.4                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Remora.Commands" Version="9.0.4" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Remora.Commands --version 9.0.4                
#r "nuget: Remora.Commands, 9.0.4"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Remora.Commands as a Cake Addin
#addin nuget:?package=Remora.Commands&version=9.0.4

// Install Remora.Commands as a Cake Tool
#tool nuget:?package=Remora.Commands&version=9.0.4                

Remora.Commands

Remora.Commands is a platform-agnostic command library that handles parsing and dispatch of typical *nix getopts-style command invocations - that is, Remora.Commands turns this:

!things add new-thing --description "My thing!" --enable-indexing

into a call to this:

[Group("things")]
public class ThingCommands : CommandGroup
{
    public Task<IResult> AddThing
    (
        string name, // = "new-thing"
        [Option("description") string description, // = "My thing!"
        [Switch("enable-indexing") bool enableIndexing = false // = true
    )
    {
        return _thingService.AddThing(name, description, enableIndexing);
    }
}

Familiar Syntax

Inspired by and closely following *nix-style getopts syntax, Remora.Commands allows you to define commands in a variety of ways, leveraging familiar and widespread principles of command-line tooling. Currently, the library supports the following option syntax types:

  • Positional options
    • T value
  • Named options
    • [Option('v')] T value (short)
    • [Option("value")] T value (long)
    • [Option('v', "value")] T value (short and long)
  • Switches
    • [Switch('e')] bool value = false (short)
    • [Switch("enable")] bool value = false (long)
    • [Switch('e', "enable")] bool value = false (short and long)
  • Collections
    • IEnumerable<T> values (positional)
    • [Option('v', "values")] IEnumerable<T> values (named, as above)
    • [Range(Min = 1, Max = 2)] IEnumerable<T> values (constrained)
  • Verbs

Short names are specified using a single dash (-), and long options by two dashes (--). As an added bonus, you may combine short-name switches, similar to what's supported by GNU tar. You can even place a normal named option at the end, followed by a value.

That is, both of the invocations below are valid (consider x, v and z as switches, and f as a named option).

my-command -xvz
my-command -xvf file.bin

The library also supports "greedy" options, which can simplify usage in certain cases, allowing users to omit quotes. A greedy option treats multiple subsequent values as one combined value, concatenating them automatically. Concatenation is done with a single space in between each value.

Making an option greedy is a simple matter of applying the Greedy attribute, and it can be combined with Option for both named and positional greedy parameters. Collections and switches, for which the greedy behaviour makes little sense, simply ignore the attribute.

[Greedy] T value

Ease of use

It's dead easy to get started with Remora.Commands.

  1. Declare a command group. Groups may be nested to form verbs, or chains of prefixes to a command.
    [Group("my-prefix")]
    public class MyCommands : CommandGroup
    {
    }
    
  2. Declare a command
    [Group("my-prefix")]
    public class MyCommands : CommandGroup
    {
        [Command("my-name")]
        public Task<IResult> MyCommand()
        {
            // ...
        }
    }
    
  3. Set up the command service with dependency injection
    var services = new ServiceCollection()
        .AddCommands()
        .AddCommandTree()
            .WithCommandGroup<MyCommands>()
            .Finish()
        .BuildServiceProvider();
    
  4. From any input source, parse and execute!
    private readonly CommandService _commandService;
    
    public async Task<IResult> MyInputHandler
    (
        string userInput, 
        CancellationToken ct
    )
    {
        var executionResult = await _commandService.TryExecuteAsync
        (
            userInput,
            ct: ct
        );
    
        if (executionResult.IsSuccess)
        {
            return executionResult;
        }
    
        _logger.Error("Oh no!");
        _logger.Error("Anyway");
    }
    

Flexibility

Command groups can be nested and combined in countless ways - registering multiple groups with the same name merges them under the same prefix, nameless groups merge their commands with their outer level, and completely different command group classes can share their prefixes unhindered.

For example, the structure below, when registered...

[Group("commands"]
public class MyFirstGroup : CommandGroup 
{
    [Command("do-thing")]
    public Task<IResult> MyCommand() { }
}

[Group("commands"]
public class MySecondGroup : CommandGroup 
{
    [Group("subcommands")
    public class MyThirdGroup : CommandGroup
    {
        [Command("do-thing")]
        public Task<IResult> MyCommand() { }
    }
}

produces the following set of available commands:

commands do-thing
commands subcommands do-thing

Generally, types return Task<IResult>, but you can use both ValueTask<T> and Task<T>, as long as T implements IResult.

Commands themselves can be overloaded using normal C# syntax, and the various argument syntax variants (that is, positional, named, switches, and collections) can easily be mixed and matched.

Even the types recognized and parsed by Remora.Commands can be extended using AbstractTypeParser<TType> - if you can turn a string into an instance of your type, Remora.Commands can parse it.

public class MyParser : AbstractTypeParser<MyType>
{
    public override ValueTask<RetrieveEntityResult<MyType>> TryParse
    (
        string value, 
        CancellationToken ct
    )
    {
        return new ValueTask<RetrieveEntityResult<MyType>>
        (
            !MyType.TryParse(value, out var result)
            ? RetrieveEntityResult<short>.FromError
              (
                  $"Failed to parse \"{value}\" as an instance of MyType."
              )
            : RetrieveEntityResult<short>.FromSuccess(result)
        );
    }
}
var services = new ServiceCollection()
    .AddCommands()
    .AddCommandTree()
        .WithCommandGroup<MyCommands>()
        .Finish()
    .AddSingletonParser<MyParser>()
    .BuildServiceProvider();

And, since parsers are instantiated with dependency injection, you can even create parsers that fetch entities from a database, that look things up online, that integrate with the rest of your application seamlessly... the possibilities are endless!

By default, Remora.Commands provides builtin parsers for the following types:

  • string
  • char
  • bool
  • byte
  • sbyte
  • ushort
  • short
  • uint
  • int
  • ulong
  • long
  • float
  • double
  • decimal
  • BigInteger
  • DateTimeOffset

Multiple trees

If your application requires different sets of commands for different contexts, you can register multiple separate trees and selectively execute commands from them. This is excellent for things where you might have a single application serving multiple users or groups thereof.

var services = new ServiceCollection()
    .AddCommands()
    .AddCommandTree()
        .WithCommandGroup<MyCommands>()
        .Finish()
    .AddCommandTree("myothertree")
        .WithCommandGroup<MyOtherCommands>()
        .Finish()
    .BuildServiceProvider();

These trees can then be accessed using the CommandTreeAccessor service. If you don't need multiple trees, or you want to expose a set of default commands, there's an unnamed tree available by default (accessed by either null or Constants.DefaultTreeName as the tree name). This is also the tree accessed by not providing a name to AddCommandTree.

var accessor = services.GetRequiredService<CommandTreeAccessor>();
if (accessor.TryGetNamedTree("myothertree", out var tree))
{
    ...
}

if (accessor.TryGetNamedTree(null, out var defaultTree))
{
    ...
}

The effects of each AddCommandTree call are cumulative, so if you want to configure the groups that are part of a tree from multiple locations (such as plugins), simply call AddCommandTree again with the same name.

services
    .AddCommands()
    .AddCommandTree("myothertree")
        .WithCommandGroup<MyOtherCommands>();

// elsewhere...

services
    .AddCommandTree("myothertree")
        .WithCommandGroup<MoreCommands>();

This would result in a tree with both MyOtherCommands and MoreCommands available.

To then access the different trees, pass the desired name when attempting to execute a command.

private readonly CommandService _commandService;

public async Task<IResult> MyInputHandler
(
    string userInput, 
    CancellationToken ct
)
{
    var executionResult = await _commandService.TryExecuteAsync
    (
        userInput,
        treeName: "myothertree",
        ct: ct
    );

    if (executionResult.IsSuccess)
    {
        return executionResult;
    }

    _logger.Error("Oh no!");
    _logger.Error("Anyway");
}

Installation

Get it on NuGet!

Thanks

Heavily inspired by CommandLineParser, a great library for parsing *nix getopts-style arguments from the command line itself.

Icon by Twemoji, licensed under CC-BY 4.0.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  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 netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on Remora.Commands:

Package Downloads
Remora.Discord.Commands

Glue code for using Remora.Commands with Remora.Discord

Coca.Remora.Valour

The glue between Valour.Api and Remora.Commands

VTP.RemoraHelpSystem

Provides a simple, modular help system for Remora.Discord.

NosSmooth.ChatCommands

Package Description

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on Remora.Commands:

Repository Stars
Remora/Remora.Discord
A data-oriented C# Discord library, focused on high-performance concurrency and robust design.
Version Downloads Last updated
10.0.6 224 7/23/2024
10.0.5 59,114 11/20/2023
10.0.4 47,045 11/14/2023
10.0.3 60,250 11/20/2022
10.0.2 23,018 8/19/2022
10.0.1 11,919 6/26/2022
10.0.0 9,260 6/8/2022
9.0.7 7,308 5/10/2022
9.0.6 471 5/9/2022
9.0.5 2,937 5/8/2022
9.0.4 6,229 4/19/2022
9.0.3 461 4/19/2022
9.0.2 10,079 3/5/2022
9.0.1 6,355 2/13/2022
9.0.0 3,176 1/18/2022
8.0.0 1,615 1/12/2022
7.2.0 37,568 12/23/2021
7.1.5 1,985 11/27/2021
7.1.4 2,424 11/9/2021
7.1.3 311 11/9/2021
7.1.2 4,987 10/15/2021
7.1.1 2,389 9/30/2021
7.1.0 747 9/27/2021
7.0.2 3,075 9/8/2021
7.0.1 2,072 9/4/2021
7.0.0 369 9/4/2021
6.1.0 896 8/26/2021
6.0.1 2,526 8/20/2021
6.0.0 341 8/20/2021
5.2.0 355 8/19/2021
5.1.0 2,303 8/11/2021
5.0.0 658 8/7/2021
4.2.0 1,981 7/31/2021
4.1.1 3,350 7/12/2021
4.1.0 402 7/12/2021
4.0.0 2,325 5/30/2021
3.1.0 3,019 4/15/2021
3.0.1 1,942 3/29/2021
3.0.0 740 3/29/2021
2.4.1 783 3/28/2021
2.4.0 2,257 3/22/2021
2.3.0 793 3/16/2021
2.2.0 1,908 3/11/2021
2.1.1 2,811 2/22/2021
2.1.0 1,746 2/12/2021
2.0.4 767 2/9/2021
2.0.3 390 2/9/2021
2.0.2 393 2/9/2021
2.0.1 331 2/8/2021
2.0.0 362 2/8/2021
1.4.0 1,666 1/20/2021
1.3.2 845 1/10/2021
1.3.1 674 1/10/2021
1.3.0 1,211 1/1/2021
1.2.2 1,424 12/26/2020
1.2.1 434 12/25/2020
1.2.0 423 12/25/2020
1.1.0 901 12/23/2020
1.0.1 468 12/22/2020
1.0.0-beta9 371 12/22/2020
1.0.0-beta8 265 12/22/2020
1.0.0-beta7 316 12/21/2020
1.0.0-beta6 280 12/21/2020
1.0.0-beta5 309 12/20/2020
1.0.0-beta4 330 11/6/2020
1.0.0-beta3 772 11/3/2020
1.0.0-beta2 297 10/26/2020
1.0.0-beta1 334 10/26/2020

Upgrade nuget packages.
           Use nuget-provided nullability checking instead of rolling our own. This fixes some odd edge cases.