Console.Routing 3.3.0-beta-6

This is a prerelease version of Console.Routing.
dotnet add package Console.Routing --version 3.3.0-beta-6                
NuGet\Install-Package Console.Routing -Version 3.3.0-beta-6                
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="Console.Routing" Version="3.3.0-beta-6" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Console.Routing --version 3.3.0-beta-6                
#r "nuget: Console.Routing, 3.3.0-beta-6"                
#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 Console.Routing as a Cake Addin
#addin nuget:?package=Console.Routing&version=3.3.0-beta-6&prerelease

// Install Console.Routing as a Cake Tool
#tool nuget:?package=Console.Routing&version=3.3.0-beta-6&prerelease                

Console.Routing

Console.Routing is a framework that makes it easy to build command line tools. Proper command line parsing can be tricky and time consuming. This library helps you implement a command line tool with almost no overhead.

Console.Routing works with a router similar to ASP.NET. Commands and parameters from the command line are routed to a specific C# method and fills in all method parameters with the matching command line arguments.

Setup and discovery

By adding two lines of code to Program.cs, you enable route discovery and handling of arguments.

 using ConsoleRouting;
 
 Routing.Handle(args);

The examples below describe commands using a fictitious tool named tool.

Discovery

You can expose any method to command line argument with the [Command] attribute. To avoid accidental exposure, the declaring class must be marked with [Module]. This example is the bare minimum to get your first command up-and-running.

[Module]
public class MyTool
{
    [Command]
    public void Hello()
    {
        Console.WriteLine("Hello world!");
    }
}

Which can be executed on the command line with:

> tool hello
Hello world!

Parameters

String parameters

To interpret a command line parameter as a string, you can add a string parameter to your command method.

> tool hello John
    [Command]
    public void Hello(string name)
    {
        Console.Writeline("Hello " + name);
    }
	

Flag parameters

Flags are used to set a switch to true. Each of the following two statements will route to method below:

> tool hello John 
> tool hello John --upper
    [Command]
    public void Hello(string name, Flag upper)
    {
        if (upper) name = name.ToUpper();
        Console.Writeline("Hello " + name);
    }

The Flag has a default cast to bool. So you can also use a bool parameter. The following method has the same command line signature as above.

    [Command]
    public void Hello(string name, bool upper)
    {
        if (upper) name = name.ToUpper();
        Console.Writeline("Hello " + name);
    }

Aligning with linux style arguments, you can abbreviate flags by using a single dash, and the first letter.

> tool hello John -u

You can group multiple letter flags:

> tool feed -cdr
   
    [Command]
    public void Feed(Flag cat, Flag dog, Flag rabbit)
    {
    
    }

Flag parameters with a value

In some tools it's common to add an argument that comes with a flag. A good exmple of this is the equivalent of Git's commit message.

> tool log --message "First commit"
> tool log -m "First commit"

You can create this behaviour with a Flag<string> parameter.

    [Command]
    public void Log(Flag<string> message)
    {
        Logger.Log(message); 
    }

Notice that the Flag<T> has an implicit cast to T.

The Flag<T> implementation currently also supports int:

> tool loop --count 5
    [Command]
    public void Loop(Flag<int> count)
    { 
    	... 
    }

And it supports enums, which are interpreted case insensitive:

> tool paint -c yellow
    public enum Colors { Yellow, Blue }
    
    [Command]
    public void Paint(Flag<Color> color)
    {
    	... 
    }

You can check if a flag is set using:

    [Command]
    public void Paint(Flag<Color> color)
    {
    	if (color.IsSet) 
	...
    }

Assignment parameters

You can provide key value pairs (assignments) as a parameter as well:

> tool login user=john password=secret
    [Command]
    public void Login(Assignment user, Assignment password)
    { ... }

Integer parameters

Integers are also recognised as parameter types:

> tool count 5
    [Command]
    public void Count(int max)
    {
        for (int i = 0; i < count; i++) Console.WriteLine(i);
    }

If the user provides anything else than an integer, in this case, the routing will not be match this method:

Optional parameters

To make a parameter optional, you can add an [Optional] attribute to it. The value will be set to null or default if it is not provided.

> tool greet John
    [Command]
    public void Greet([Optional]string name)
    {
        if (name is null)
        {
            Console.WriteLine("Hello");
        }
        else 
        {
            Console.Writeline("Hello " + name);
        }
    }

This attribute is not necessary on any type of flag or assignment parameter since they are optional by design.

If you have the C# nullable feature enabled, you can also use that for making a parameter optional:

    [Command]
    public void Greet(string? name)
    {
	...
    }

Parameter aliases

For parameters that cannot be expressed with a csharp symbol, You can define an parameter alternative with the [Alt] attribute:

> tool debug --no-color
    [Command]
    public void Debug([Alt("no-color")] Flag nocolor)
    {
    
    }

Parameter buckets

Some times you need a large set of optional settings. For this a regular parameter list is hard to read and manage. For this, you can use classes to group those parameters. This also convenient for re-use:. You can use parameter buckets by decorating a class with a [Bucket] attribute.

    [Command]
    public void Curl(CurlSettings settings)
    { 
    }

    [Bucket]
    class CurlSettings
    {
        public Flag CrlF;
        public bool Append;
        public Flag<string> Url;
        
        [Alt("use-ascii")]
        public Flag UseAscii { get; set; }
        public Flag Basic { get; set; }
        public Flag<string> RemoteName { get; set; }
    }

Capturing

If you want a certain parameter to capture the route, you can add a Capture attribute. If the defined capture parameter is found anywhere in a argument list, the route execution will go to that command, without further resolving other routes.

To have a capture, add a Capture attribute to a command method like this:

    [Command, Capture("--help", "-h")]
    public void Help(Args args)
    {
        Console.WriteLine($"Help for these arguments: {args}");
    }

Global Settings

You can mark any static class as a global settings class. This allows you to use the same parameters on all commands in your tool. Only properties (not fields) will be set when a matching name is found. And currently only bool is supported (a flag on the command line) But it's the plan to add string and int (valued flags) later.

For this, use the [Global] flag.

[Global]
public static class Settings
{
    public static bool Debug { get; set; }
    public static bool Verbose { get; set; }
}

You can have multple static classes marked as global.

Commands

Command Overloading

You can overload your commands. So if you provide two commands with the same name, but different parameter types, or different parameter count. the proper command route will be found:

> tool count
> tool count 3
> tool count Dracula
> tool count your luck

Each of the above inputs, will route to a different method below:

    [Command]
    public void Count()
    { ... }

    [Command]
    public void Count(int number)
    { ...  }
    
    [Command]
    public void Count(string name)
    { ...  }

    [Command]
    public void Count(string whos, string item)
    { ... }

Command name aliases

As an opposite of overloading, Console.Routing allows a single command to have multiple aliases.

> tool greet Anna
> tool greeting Anna

Just like the default command names, matching is case insensitive. Bare in mind, that if you add an alias, you should also provide the original name in the list, if you want to make that work as well.

    [Command("greet", "greeting")]
    public void Greet(string name)
    {
        Console.Writeline("Hello " + name);
    }

Aliases also allow you to use commands that C# syntax do not allow. If you must you can parse a flag as a command, but keep in mind that it will just be treated as a literal, so if you need the abbreviation too, you have to add it yourself.

    [Command("info", "give-info", "--info", "-i")]
    public void Info(string name)
    {
        Console.Writeline("Hello " + name);
    }

Hidden commands

If you want a command to be usable, but not showing up in the help, you can use the [Hidden] attribute:

    [Command, Hidden]
    public void Secret()
    { ... }

Default command

You should always provide a command that responds when the user has given no additinal input at all. This command can also be used for root flags: if no command or sub command has been given.

> tool --help
> tool --info
    [Command, Default]
    public void Info(Flag help, Flag version)
    {
        if (help.Set) ShowHelp();
        if (version.Set) ShowVersion();
    }

Nested Commands

You can create nested commands, or command groups, by marking a Module class as a command in itself:

> tool database update
> tool database drop

This example (borrowed from Entity Frameowrk command line tool), can be constructed like this:

[Module, Command]
public class Database
{
    [Command]
    public void Update() { }
    
    [Command]
    public void Drop() { }
}

You can create deeper nested commands by creating sub classes.

[Module, Command]
public class Main
{
    [Command]
    public class Sub 
    {
        [Command]
        public void SubSub()
        {
        
        }
    }
}

Help and Documentation

Command listing information

The help command works out of the box and that lists all the available commands.

  > tool help
  > tool -h

The help text will look look something like this:

Module title:
    Help    --version | Prints this help text
    Greet   <name> | Says hello to someone

To provide a one liner help text in the command list, use the [Help(text)] attribute:

    [Command, Help("This greeting greets any provided name")]
    public void Greet(string name)
    {
        Console.WriteLine($"Hello {name}")
    }

Detailed documentation

By default you also get more detailed help using the following:

> tool help <command> 
> tool command -?

The help will be contain four segments:

  1. The route, enabled by default
  2. The description, provided by the [Help( ... )] attribute.
  3. The parameter list.
  4. The extended documentation text

C# XML Documentation

Note that to make full use of this feature, you should enable C# XML inline documentation in your project. You can enable xml documentation to be published with your tool - necessary for the help enrichment to work, you have to enable it either your build settings (Visual Studio → Project → properties → Build → Output → Xml Documentation file) or directly in the .csproj file of the app or dll where your commands reside:

  <PropertyGroup>
	  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

Example

A fully documentation enriched command will look something like this:

        /// <summary>Says hello to the person in question</summary>
        /// <param name="name"> The name of the person that will be greeted. You can use any name in the known universe </param>
        /// <param name="uppercase">Transforms the name to all caps</param>
        /// <param name="repeats">How many times the greeting should be repeated</param>
        [Command, Help("Says hello to the given name")]
        public void Greet([Optional]string name, bool uppercase, Flag<int> repeats)
        {
            if (uppercase) name = name.ToUpper();
            for(int i = 1; i < (repeats.HasValue ? repeats.Value : 1); i++)
            {
                Console.WriteLine($"Hello {name}!");
            }
        }

This example produces

> tool greet -?
Command:
  Greet (<name>) --uppercase --repeats <value>

Description:
Says hello to the given name

Parameters:
  (<name>)             The name that will be greeted. You can use any name in the known universe
  --uppercase          Transforms the name to all caps
  --repeats <value>    How many times the greeting should be repeated

Documentation:
Says hello to the name in question

Write your own help

You can replace the default help by providing your own implementation, and not letting the route builder discover the default. This is the default startup

  Routing.Hanlde(args);

In the background that runs a command that looks something like this:

    var router = new RouterBuilder()
        .AddAssemblyOf<Program>()
	.AddDefaultHelp(this RouterBuilder builder)
        .Buid();

If you want to write your own implementation, you should construct a router without the default help, and write your own help method.

    [Command, Help("Prints this help text")]
    public void Help(Flag version)
    {
        Routing.PrintHelp();
    }

    [Command, Help("Says hello to name")]
    public void Greet(string name)
    { ... }

You can invoke the default help documentation by calling PrintHelp. You can also write your own. If you need the data from the router, you can have the router injected inthe module like this:

  [Module]
  public class MyCommands
  {
  	Router router;
	
  	public MyCommands(Router router)
	{
	      	this.router = router
	}
	
	 [Command("help"), Help("Provides this help list or detailed help about a command")]
        public void Help(Arguments args = null)
        {
            if (args is null || args.Count == 0)
            {
	    	WriteMyOwnRoutes(router);
                // router.Writer.WriteRoutes(router); <- default
            }
            else
            {
	    	var result = router.Bind(args);
	    	WriteMyOwnRouteHelp(result);
                
                // router.Writer.WriteRouteHelp(result);
            }
        } 
  }

If you also want to capture the -? or --help flags at the end, you should implement a capturing command. See below.

Configuring the router

Additional assmblies

By using the default Routing.Handle(args) all modules and commands in your startup project will be discovered as a route candidate. If you have commands in a different library (dll) or in multiple libraries, you can use the RouteBuilder class instead.

The router builder allows you to add assemblies to the discovery list, either by providing the assembly or a type in the assembly. The example below has some duplicate functionality but it shows you the options:

    var router = new RouterBuilder()
        .Add(Assembly.GetExecutingAssembly())
        .AddAssemblyOf<Program>()
        .Add(AppDomain.CurrentDomain.GetAssemblies())
        .Buid();
    
    router.Handle(args);

Dependency Injection

You can add also services available for dependency injection through the RouterBuilder. To add a service, use the .AddService() method.

    var router = new RouterBuilder()
        .AddAssemblyOf<Program>()
        .AddService<SomeService>()
        .Buid();
	

Which can then be used in your command modules:

  [Module]
  public class MyCommands
  {
     public MyCommand(SomeService service)  
     { ... }
     
     [Command]
     public Action()
     {
         service.DoWork();
     }
  }

Custom Exception handling

You can define your own exception handling, using the following extension on the route builder.

  .AddExceptionHandler(Action<Router, Exception> handler)
        
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
3.3.0-beta-6 184 8/16/2023
3.3.0-beta-5 131 4/23/2023
3.3.0-beta-4 135 4/23/2023
3.3.0-beta-2 161 6/12/2022
3.3.0-beta-1 161 6/5/2022
3.2.2 763 4/7/2022
3.2.0 431 11/1/2021
3.1.2 322 10/14/2021
3.1.1 318 10/14/2021
3.1.0 354 10/13/2021
3.1.0-beta7 241 9/3/2021
3.1.0-beta6 236 9/3/2021
3.1.0-beta5 252 3/17/2021
3.1.0-beta4 255 3/15/2021
3.0.0 434 9/8/2020
3.0.0-beta9 294 7/16/2020
3.0.0-beta8 334 7/9/2020
3.0.0-beta7 355 7/9/2020
3.0.0-beta6 368 7/9/2020
3.0.0-beta5 320 7/7/2020
3.0.0-beta4 416 7/5/2020
3.0.0-beta3 368 7/3/2020
3.0.0-beta2 291 7/2/2020

# Default parameters
Default parameter values are now respected for optional parameters

# Capturehelp visible
The capturehelp command 'hijacks' a command path,
and reroutes it to the help commmand. But this command was visible in the help.
It's made invisible

# Shorter help
Help was too verbose. the default help command list display is now
without parameters. They are still in the expanded help for a specific command.
And you can still use the old `RoutingWriter.WriteRoutes(..)`

# Help distinct
Overloaded commands are displayed only once in the shorter help
   
# Uppercase flags
Flags are now treated as case sensitive when abbreviated.